Skip to content

Commit b9068c3

Browse files
committed
Improve preloaded reference detection
1 parent 5e8784d commit b9068c3

File tree

10 files changed

+159
-64
lines changed

10 files changed

+159
-64
lines changed

src/Record.php

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ class Record implements \ArrayAccess
1616

1717
private $schema;
1818

19+
private $primaryKey;
20+
1921
private $values;
2022

2123
private $changed;
2224

2325
private $state;
2426

27+
/** @var Record[][] */
2528
private $relations;
2629

2730
public function __construct(Schema $schema)
@@ -30,17 +33,16 @@ public function __construct(Schema $schema)
3033
$this->values = array_fill_keys($schema->getFields(), null);
3134
$this->state = self::STATE_INSERT;
3235
$this->changed = [];
36+
$this->relations = [];
3337
}
3438

35-
public function getPrimaryKeys(): array
39+
public function getPrimaryKey(): array
3640
{
37-
$primaryKey = [];
38-
39-
foreach ($this->schema->getPrimaryKeys() as $key) {
40-
$primaryKey[$key] = $this->values[$key];
41+
if (empty($this->primaryKey)) {
42+
throw new \RuntimeException('Cannot refer to the record via primary key, if it is not defined');
4143
}
4244

43-
return $primaryKey;
45+
return $this->primaryKey;
4446
}
4547

4648
public function isNew(): bool
@@ -57,6 +59,17 @@ public function updateState(int $state): void
5759
{
5860
$this->state = $state === self::STATE_DELETE ? self::STATE_DELETE : self::STATE_UPDATE;
5961
$this->changed = [];
62+
63+
$this->updatePrimaryKey();
64+
}
65+
66+
private function updatePrimaryKey(): void
67+
{
68+
$this->primaryKey = [];
69+
70+
foreach ($this->schema->getPrimaryKey() as $key) {
71+
$this->primaryKey[$key] = $this->values[$key];
72+
}
6073
}
6174

6275
public function getSchema(): Schema
@@ -69,6 +82,11 @@ public function getModel(): Model
6982
return $this->schema->getModel($this);
7083
}
7184

85+
public function isReferenceLoaded(string $name): bool
86+
{
87+
return isset($this->relations[$name]);
88+
}
89+
7290
/**
7391
* @param string $name
7492
* @return Record[]
@@ -84,44 +102,59 @@ public function getReference(string $name): array
84102

85103
public function fillReference(string $name, array $records): void
86104
{
87-
$relation = $this->getSchema()->getReference($name);
105+
$reference = $this->getSchema()->getReference($name);
88106

89107
foreach ($records as $record) {
90-
if (!$this->isRelated($relation, $record)) {
108+
if (!$this->isRelated($reference, $record)) {
91109
throw new \InvalidArgumentException('The provided records are not related to this record');
92110
}
93111
}
94112

95-
if (\count($records) > 1 && $relation->isSingleRelationship()) {
113+
if (\count($records) > 1 && $reference->isSingleRelationship()) {
96114
throw new \InvalidArgumentException('The relationship cannot reference more than a single record');
97115
}
98116

99117
$this->relations[$name] = array_values($records);
100118
}
101119

102-
public function isReferenceLoaded(string $name): bool
120+
private function isRelated(Reference $reference, Record $record): bool
103121
{
104-
return isset($this->relations[$name]);
105-
}
106-
107-
private function isRelated(Reference $relation, Record $record): bool
108-
{
109-
if ($relation->getReferencedSchema() !== $record->getSchema()) {
122+
if ($reference->getReferencedSchema() !== $record->getSchema()) {
110123
return false;
111124
}
112125

113-
$keys = $relation->getFields();
114-
$references = $relation->getReferencedFields();
126+
$keys = $reference->getFields();
127+
$fields = $reference->getReferencedFields();
115128

116-
while ($keys) {
117-
if (!$relation->matchValues($this->values[array_pop($keys)], $record->values[array_pop($references)])) {
129+
foreach ($keys as $index => $key) {
130+
if ((string) $this->values[$key] !== (string) $record->values[$fields[$index]]) {
118131
return false;
119132
}
120133
}
121134

122135
return true;
123136
}
124137

138+
public function getMappedRecords(): array
139+
{
140+
/** @var Record[] $records */
141+
$records = [spl_object_id($this) => $this];
142+
143+
do {
144+
foreach (current($records)->relations as $relation) {
145+
foreach ($relation as $record) {
146+
$id = spl_object_id($record);
147+
148+
if (!isset($records[$id])) {
149+
$records[$id] = $record;
150+
}
151+
}
152+
}
153+
} while (next($records) !== false);
154+
155+
return array_values($records);
156+
}
157+
125158
public function setDatabaseValues(array $row)
126159
{
127160
if (array_keys($row) !== array_keys($this->values)) {
@@ -131,6 +164,7 @@ public function setDatabaseValues(array $row)
131164
$this->values = $row;
132165
$this->state = self::STATE_UPDATE;
133166
$this->changed = [];
167+
$this->updatePrimaryKey();
134168
}
135169

136170
public function getDatabaseValues(): array
@@ -163,10 +197,6 @@ public function offsetSet($offset, $value)
163197
throw new \InvalidArgumentException("Invalid record field '$offset'");
164198
}
165199

166-
if ($this->state === self::STATE_UPDATE && \in_array($offset, $this->schema->getPrimaryKeys(), true)) {
167-
throw new \RuntimeException('Cannot change values of primary keys for saved records');
168-
}
169-
170200
$this->values[$offset] = $value;
171201
$this->changed[$offset] = true;
172202
}

src/Reference.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,23 @@ class Reference
1414
private $fields;
1515
private $referencedSchema;
1616
private $referencedFields;
17+
private $primaryRelation;
1718

1819
public function __construct(Schema $schema, array $fields, Schema $referencedSchema, array $referencedFields)
1920
{
21+
$formatStrings = function (string ... $strings): array {
22+
return $strings;
23+
};
24+
2025
$this->schema = $schema;
21-
$this->fields = $fields;
26+
$this->fields = $formatStrings(... $fields);
2227
$this->referencedSchema = $referencedSchema;
23-
$this->referencedFields = $referencedFields;
28+
$this->referencedFields = $formatStrings(... $referencedFields);
29+
$this->primaryRelation = array_diff($this->referencedSchema->getPrimaryKey(), $this->referencedFields) === [];
30+
31+
if (empty($this->fields) || \count($this->fields) !== \count($this->referencedFields)) {
32+
throw new \InvalidArgumentException('Unexpected list of fields in relationship');
33+
}
2434
}
2535

2636
public function getSchema(): Schema
@@ -43,13 +53,8 @@ public function getReferencedFields(): array
4353
return $this->referencedFields;
4454
}
4555

46-
public function matchValues($value, $referencedValue): bool
47-
{
48-
return (string) $value === (string) $referencedValue;
49-
}
50-
5156
public function isSingleRelationship(): bool
5257
{
53-
return array_diff($this->referencedSchema->getPrimaryKeys(), $this->referencedFields) === [];
58+
return $this->primaryRelation;
5459
}
5560
}

src/ReferenceFiller.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ public function fill(array $records, array $references): void
3939
throw new \InvalidArgumentException('The provided list of records did not share the same schema');
4040
}
4141

42-
$this->cacheRecord($schemaId, $record);
42+
foreach ($record->getMappedRecords() as $mappedRecord) {
43+
$this->cacheRecord($schemaId, $mappedRecord);
44+
}
4345
}
4446

4547
$this->fillReferences($records, $references);
@@ -64,7 +66,7 @@ private function fillReferences(array $records, array $references): void
6466
throw new \RuntimeException('Filling references for composite foreign keys is not supported');
6567
}
6668

67-
$isPrimaryReference = $fields === $parent->getPrimaryKeys();
69+
$isPrimaryReference = $fields === $parent->getPrimaryKey();
6870
$key = array_pop($keys);
6971
$field = array_pop($fields);
7072
$options = [];
@@ -75,11 +77,6 @@ private function fillReferences(array $records, array $references): void
7577

7678
if ($record->isReferenceLoaded($name)) {
7779
$sorted[$value] = $record->getReference($name);
78-
79-
foreach ($sorted[$value] as $referencedRecord) {
80-
$this->cacheRecord($schemaId, $referencedRecord);
81-
}
82-
8380
continue;
8481
}
8582

@@ -134,15 +131,20 @@ private function parseReferences(array $references): array
134131

135132
private function cacheRecord(string $schemaId, Record $record): void
136133
{
137-
$recordId = implode('-', $record->getPrimaryKeys());
134+
$recordId = implode('-', $record->getPrimaryKey());
135+
136+
if (isset($this->cache[$schemaId][$recordId])) {
137+
throw new \RuntimeException('Duplicated record detected when filling references for records');
138+
}
139+
138140
$this->cache[$schemaId][$recordId] = $record;
139141
}
140142

141143
private function getCachedRecord(string $schemaId, Schema $schema, array $row): Record
142144
{
143145
$primaryKey = [];
144146

145-
foreach ($schema->getPrimaryKeys() as $key) {
147+
foreach ($schema->getPrimaryKey() as $key) {
146148
$primaryKey[] = $row[$key];
147149
}
148150

src/Repository.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ protected function find(Schema $schema, array $conditions, array $order = [], in
3535

3636
protected function findOne(Schema $schema, array $conditions): ?Model
3737
{
38-
$keys = $schema->getPrimaryKeys();
38+
$keys = $schema->getPrimaryKey();
3939

4040
if ($keys) {
4141
$order = array_fill_keys($keys, Connection::ORDER_ASCENDING);
@@ -58,7 +58,7 @@ protected function findByPrimaryKey(Schema $schema, $values): ?Model
5858

5959
protected function getPrimaryKeyCondition(Schema $schema, $values)
6060
{
61-
$keys = $schema->getPrimaryKeys();
61+
$keys = $schema->getPrimaryKey();
6262
$condition = [];
6363

6464
if (!\is_array($values)) {
@@ -106,7 +106,7 @@ protected function insert(Model $model)
106106
$schema = $record->getSchema();
107107
$values = $record->getDatabaseValues();
108108

109-
$primaryKeys = $schema->getPrimaryKeys();
109+
$primaryKeys = $schema->getPrimaryKey();
110110

111111
if (\count($primaryKeys) === 1) {
112112
$primary = reset($primaryKeys);
@@ -131,7 +131,7 @@ protected function update(Model $model)
131131
$schema = $record->getSchema();
132132
$values = array_intersect_key($record->getDatabaseValues(), array_flip($record->getChangedFields()));
133133

134-
$this->connection->update($schema->getTable(), $values, $record->getPrimaryKeys());
134+
$this->connection->update($schema->getTable(), $values, $record->getPrimaryKey());
135135
$record->updateState(Record::STATE_UPDATE);
136136
}
137137

@@ -140,7 +140,7 @@ protected function delete(Model $model)
140140
$record = $model->getDatabaseRecord();
141141
$schema = $record->getSchema();
142142

143-
$this->connection->delete($schema->getTable(), $record->getPrimaryKeys());
143+
$this->connection->delete($schema->getTable(), $record->getPrimaryKey());
144144
$record->updateState(Record::STATE_DELETE);
145145
}
146146

src/Schema.php

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,28 @@ class Schema
1919

2020
protected $table;
2121

22-
protected $primaryKeys;
22+
protected $primaryKey;
2323

2424
protected $fields;
2525

2626
protected $references;
2727

28+
private $referenceCache;
29+
2830
public function __construct(ContainerInterface $container)
2931
{
3032
$this->container = $container;
33+
$this->referenceCache = [];
3134
}
3235

3336
public function getTable(): string
3437
{
3538
return $this->table;
3639
}
3740

38-
public function getPrimaryKeys(): array
41+
public function getPrimaryKey(): array
3942
{
40-
return $this->primaryKeys;
43+
return (array) $this->primaryKey;
4144
}
4245

4346
public function getFields(): array
@@ -47,16 +50,22 @@ public function getFields(): array
4750

4851
public function getReference(string $name): Reference
4952
{
53+
if (isset($this->referenceCache[$name])) {
54+
return $this->referenceCache[$name];
55+
}
56+
5057
if (!isset($this->references[$name])) {
5158
throw new \InvalidArgumentException("Invalid reference '$name'");
5259
}
5360

54-
return new Reference(
61+
$this->referenceCache[$name] = new Reference(
5562
$this,
56-
$this->references[$name]['keys'],
63+
(array) $this->references[$name]['key'],
5764
$this->getSchema($this->references[$name]['schema']),
58-
$this->references[$name]['fields']
65+
(array) $this->references[$name]['field']
5966
);
67+
68+
return $this->referenceCache[$name];
6069
}
6170

6271
private function getSchema(string $name): Schema

tests/helpers/IntegrationTestCase.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,27 @@ public function testLoadingReferences(): void
188188
return $model->getFirstName();
189189
};
190190

191-
$names = array_map($firstName, $person->getParents());
191+
$childRecords = function (TestPersonModel $model): array {
192+
$childRecords = [];
193+
194+
foreach ($model->getChildren() as $child) {
195+
$childRecords[] = $child->getDatabaseRecord();
196+
}
197+
198+
return $childRecords;
199+
};
200+
201+
$parents = $person->getParents();
202+
203+
$this->assertCount(2, $parents);
204+
205+
$names = array_map($firstName, $parents);
192206
sort($names);
193207

194208
$this->assertSame(['Mama', 'Papa'], $names);
209+
210+
foreach ($parents as $parent) {
211+
$this->assertTrue(\in_array($person->getDatabaseRecord(), $childRecords($parent), true));
212+
}
195213
}
196214
}

0 commit comments

Comments
 (0)