Skip to content

Commit e58e15d

Browse files
committed
Add reverse relationship filling
1 parent 648723e commit e58e15d

File tree

9 files changed

+219
-125
lines changed

9 files changed

+219
-125
lines changed

src/Record.php

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class Record implements \ArrayAccess
2727
/** @var Record[][] */
2828
private $references;
2929

30+
private $model;
31+
3032
public function __construct(Schema $schema)
3133
{
3234
$this->schema = $schema;
@@ -79,37 +81,41 @@ public function getSchema(): Schema
7981

8082
public function getModel(): Model
8183
{
82-
return $this->schema->getModel($this);
84+
if ($this->model === null) {
85+
$this->model = $this->schema->getModel($this);
86+
}
87+
88+
return $this->model;
8389
}
8490

85-
public function getReferredModel(string $name): Model
91+
public function getReferencedModel(string $name): Model
8692
{
87-
$reference = $this->getSchema()->getReference($name);
93+
$relationship = $this->getSchema()->getRelationship($name);
8894

89-
if (!$reference->isSingleRelationship()) {
90-
throw new \RuntimeException('Can only refer to single models in a single relationship');
95+
if (!$relationship->isUniqueRelationship()) {
96+
throw new \RuntimeException('Cannot fetch a single model a non-unique relationship');
9197
}
9298

93-
$records = $this->getReference($name);
99+
$records = $this->getReferencedRecords($name);
94100

95101
if (empty($records)) {
96-
throw new \UnexpectedValueException('The single relationship does not refer to any record');
102+
return null;
97103
}
98104

99-
return $records[0]->getModel();
105+
return $this->getReferencedRecords($name)[0]->getModel();
100106
}
101107

102108
public function getReferredModels(string $name): array
103109
{
104-
$reference = $this->getSchema()->getReference($name);
110+
$reference = $this->getSchema()->getRelationship($name);
105111

106-
if ($reference->isSingleRelationship()) {
112+
if ($reference->isUniqueRelationship()) {
107113
throw new \RuntimeException('Cannot refer to multiple models in a single relationship');
108114
}
109115

110116
$models = [];
111117

112-
foreach ($this->getReference($name) as $record) {
118+
foreach ($this->getReferencedRecords($name) as $record) {
113119
$models[] = $record->getModel();
114120
}
115121

@@ -118,21 +124,21 @@ public function getReferredModels(string $name): array
118124

119125
public function getReferredProxyModels(string $proxy, string $name): array
120126
{
121-
$proxyReference = $this->getSchema()->getReference($proxy);
122-
$reference = $proxyReference->getReferencedSchema()->getReference($name);
127+
$proxyReference = $this->getSchema()->getRelationship($proxy);
128+
$reference = $proxyReference->getReferencedSchema()->getRelationship($name);
123129

124-
if ($proxyReference->isSingleRelationship()) {
130+
if ($proxyReference->isUniqueRelationship()) {
125131
throw new \RuntimeException('Cannot refer to multiple models in a single relationship');
126132
}
127133

128-
if (!$reference->isSingleRelationship()) {
134+
if (!$reference->isUniqueRelationship()) {
129135
throw new \RuntimeException('Can only refer to single models in a single relationship');
130136
}
131137

132138
$models = [];
133139

134-
foreach ($this->getReference($proxy) as $record) {
135-
$records = $record->getReference($name);
140+
foreach ($this->getReferencedRecords($proxy) as $record) {
141+
$records = $record->getReferencedRecords($name);
136142

137143
if (empty($records)) {
138144
throw new \UnexpectedValueException('The single relationship does not refer to any record');
@@ -144,7 +150,7 @@ public function getReferredProxyModels(string $proxy, string $name): array
144150
return $models;
145151
}
146152

147-
public function isReferenceLoaded(string $name): bool
153+
public function hasReferencedRecords(string $name): bool
148154
{
149155
return isset($this->references[$name]);
150156
}
@@ -153,33 +159,45 @@ public function isReferenceLoaded(string $name): bool
153159
* @param string $name
154160
* @return Record[]
155161
*/
156-
public function getReference(string $name): array
162+
public function getReferencedRecords(string $name): array
157163
{
158164
if (!isset($this->references[$name])) {
159-
throw new \RuntimeException("Cannot access relation '$name' that has not been provided");
165+
throw new \RuntimeException("The referenced records for the relationship '$name' have not been filled");
160166
}
161167

162168
return $this->references[$name];
163169
}
164170

165-
public function fillReference(string $name, array $records): void
171+
/**
172+
* @param string $name
173+
* @param Record[] $records
174+
*/
175+
public function fillReferencedRecords(string $name, array $records): void
166176
{
167-
$reference = $this->getSchema()->getReference($name);
177+
$relationship = $this->getSchema()->getRelationship($name);
178+
179+
if (\count($records) > 1 && $relationship->isUniqueRelationship()) {
180+
throw new \InvalidArgumentException('A unique relationship cannot reference more than a single record');
181+
}
168182

169183
foreach ($records as $record) {
170-
if (!$this->isRelated($reference, $record)) {
184+
if (!$this->isRelated($relationship, $record)) {
171185
throw new \InvalidArgumentException('The provided records are not related to this record');
172186
}
173187
}
174188

175-
if (\count($records) > 1 && $reference->isSingleRelationship()) {
176-
throw new \InvalidArgumentException('The relationship cannot reference more than a single record');
189+
if ($relationship->getReverseRelationship()->isUniqueRelationship()) {
190+
$reverse = $relationship->getReverseRelationship()->getName();
191+
192+
foreach ($records as $record) {
193+
$record->references[$reverse] = [$this];
194+
}
177195
}
178196

179197
$this->references[$name] = array_values($records);
180198
}
181199

182-
private function isRelated(Reference $reference, Record $record): bool
200+
private function isRelated(Relationship $reference, Record $record): bool
183201
{
184202
if ($reference->getReferencedSchema() !== $record->getSchema()) {
185203
return false;

src/Reference.php

Lines changed: 0 additions & 68 deletions
This file was deleted.

src/ReferenceFiller.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ private function fillReferences(array $records, array $references): void
5555
$schema = reset($records)->getSchema();
5656

5757
foreach ($this->parseReferences($references) as $name => $childReferences) {
58-
$reference = $schema->getReference($name);
58+
$reference = $schema->getRelationship($name);
5959
$keys = $reference->getFields();
6060
$fields = $reference->getReferencedFields();
6161
$parent = $reference->getReferencedSchema();
@@ -65,6 +65,7 @@ private function fillReferences(array $records, array $references): void
6565
throw new \RuntimeException('Filling references for composite foreign keys is not supported');
6666
}
6767

68+
$fillRecords = [];
6869
$isPrimaryReference = $fields === $parent->getPrimaryKey();
6970
$key = array_pop($keys);
7071
$field = array_pop($fields);
@@ -74,17 +75,21 @@ private function fillReferences(array $records, array $references): void
7475
foreach ($records as $record) {
7576
$value = $record[$key];
7677

77-
if ($record->isReferenceLoaded($name)) {
78-
$sorted[$value] = $record->getReference($name);
78+
if ($record->hasReferencedRecords($name)) {
79+
$sorted[$value] = $record->getReferencedRecords($name);
7980
continue;
8081
}
8182

83+
$fillRecords[] = $record;
84+
8285
if ($isPrimaryReference && isset($this->cache[$schemaId][$value])) {
8386
$sorted[$value] = [$this->cache[$schemaId][$value]];
8487
continue;
8588
}
8689

87-
$options[$value] = true;
90+
if ($value !== null) {
91+
$options[$value] = true;
92+
}
8893
}
8994

9095
$options = array_keys(array_diff_key($options, $sorted));
@@ -99,8 +104,8 @@ private function fillReferences(array $records, array $references): void
99104
}
100105
}
101106

102-
foreach ($records as $record) {
103-
$record->fillReference($name, $sorted[$record[$key]] ?? []);
107+
foreach ($fillRecords as $record) {
108+
$record->fillReferencedRecords($name, $sorted[$record[$key]] ?? []);
104109
}
105110

106111
if ($sorted && $childReferences) {

src/Relationship.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
namespace Simply\Database;
4+
5+
/**
6+
* Relation.
7+
* @author Riikka Kalliomäki <[email protected]>
8+
* @copyright Copyright (c) 2018 Riikka Kalliomäki
9+
* @license http://opensource.org/licenses/mit-license.php MIT License
10+
*/
11+
class Relationship
12+
{
13+
private $name;
14+
private $schema;
15+
private $fields;
16+
private $referencedSchema;
17+
private $referencedFields;
18+
private $unique;
19+
private $reverse;
20+
21+
public function __construct(
22+
string $name,
23+
Schema $schema,
24+
array $fields,
25+
Schema $referencedSchema,
26+
array $referencedFields,
27+
bool $unique
28+
) {
29+
$formatStrings = function (string ... $strings): array {
30+
return $strings;
31+
};
32+
33+
$this->name = $name;
34+
$this->schema = $schema;
35+
$this->fields = $formatStrings(... $fields);
36+
$this->referencedSchema = $referencedSchema;
37+
$this->referencedFields = $formatStrings(... $referencedFields);
38+
$this->unique = $unique || array_diff($this->referencedSchema->getPrimaryKey(), $this->referencedFields) === [];
39+
40+
if (empty($this->fields) || \count($this->fields) !== \count($this->referencedFields)) {
41+
throw new \InvalidArgumentException('Unexpected list of fields in relationship');
42+
}
43+
44+
if (array_diff($this->fields, $this->schema->getFields()) !== []) {
45+
throw new \InvalidArgumentException('The referencing fields must be defined in the referencing schema');
46+
}
47+
48+
if (array_diff($this->referencedFields, $this->referencedSchema->getFields()) !== []) {
49+
throw new \InvalidArgumentException('The referenced fields must be defined in the referenced schema');
50+
}
51+
}
52+
53+
public function getName(): string
54+
{
55+
return $this->name;
56+
}
57+
58+
public function getSchema(): Schema
59+
{
60+
return $this->schema;
61+
}
62+
63+
public function getFields(): array
64+
{
65+
return $this->fields;
66+
}
67+
68+
public function getReferencedSchema(): Schema
69+
{
70+
return $this->referencedSchema;
71+
}
72+
73+
public function getReferencedFields(): array
74+
{
75+
return $this->referencedFields;
76+
}
77+
78+
public function isUniqueRelationship(): bool
79+
{
80+
return $this->unique;
81+
}
82+
83+
public function getReverseRelationship(): Relationship
84+
{
85+
if ($this->reverse === null) {
86+
$this->reverse = $this->detectReverseRelationship();
87+
}
88+
89+
return $this->reverse;
90+
}
91+
92+
private function detectReverseRelationship(): Relationship
93+
{
94+
$reverse = array_filter(
95+
$this->referencedSchema->getRelationships(),
96+
function (Relationship $relationship) {
97+
return $this->isReverseRelationship($relationship);
98+
}
99+
);
100+
101+
if (\count($reverse) !== 1) {
102+
if (\count($reverse) > 1) {
103+
throw new \RuntimeException('Multiple reverse relationship exists for this relationship');
104+
}
105+
106+
throw new \RuntimeException('No reverse relationship exists for this relationship');
107+
}
108+
109+
return array_pop($reverse);
110+
}
111+
112+
private function isReverseRelationship(Relationship $relationship): bool
113+
{
114+
return $relationship->getSchema() === $this->getReferencedSchema()
115+
&& $relationship->getReferencedSchema() === $this->getSchema()
116+
&& $relationship->getFields() === $this->getReferencedFields()
117+
&& $relationship->getReferencedFields() === $this->getFields();
118+
}
119+
}

0 commit comments

Comments
 (0)