1
|
|
|
<?php |
2
|
|
|
namespace vakata\database\schema; |
3
|
|
|
|
4
|
|
|
use vakata\collection\Collection; |
5
|
|
|
use vakata\database\DBInterface; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* A basic mapper to enable relation traversing and basic create / update / delete functionality |
9
|
|
|
*/ |
10
|
|
|
class Mapper |
11
|
|
|
{ |
12
|
|
|
protected $db; |
13
|
|
|
protected $objects; |
14
|
|
|
|
15
|
4 |
|
public function __construct(DBInterface $db) |
16
|
|
|
{ |
17
|
4 |
|
$this->db = $db; |
18
|
4 |
|
} |
19
|
|
|
/** |
20
|
|
|
* Create an entity from an array of data |
21
|
|
|
* |
22
|
|
|
* @param Table $definition |
23
|
|
|
* @param array $data |
24
|
|
|
* @param boolean $empty |
25
|
|
|
* @return object |
26
|
|
|
*/ |
27
|
36 |
|
public function entity(Table $definition, array $data, bool $empty = false) |
28
|
|
|
{ |
29
|
36 |
|
if (!$empty) { |
30
|
36 |
|
$primary = []; |
31
|
36 |
|
foreach ($definition->getPrimaryKey() as $column) { |
32
|
36 |
|
$primary[$column] = $data[$column]; |
33
|
|
|
} |
34
|
36 |
|
if (isset($this->objects[$definition->getName()][base64_encode(serialize($primary))])) { |
35
|
36 |
|
return $this->objects[$definition->getName()][base64_encode(serialize($primary))]; |
36
|
|
|
} |
37
|
|
|
} |
38
|
|
|
$entity = new class ($this, $definition, $data, $empty) extends \StdClass { |
|
|
|
|
39
|
|
|
protected $mapper; |
40
|
|
|
protected $empty; |
41
|
|
|
protected $definition; |
42
|
|
|
protected $initial = []; |
43
|
|
|
protected $changed = []; |
44
|
|
|
protected $fetched = []; |
45
|
|
|
|
46
|
19 |
|
public function __construct($mapper, $definition, array $data = []) |
47
|
|
|
{ |
48
|
19 |
|
$this->mapper = $mapper; |
49
|
19 |
|
$this->definition = $definition; |
50
|
19 |
|
$this->initial = $data; |
51
|
19 |
|
} |
52
|
22 |
|
public function __lazyProperty(string $property, $resolve) |
53
|
|
|
{ |
54
|
22 |
|
$this->fetched[$property] = $resolve; |
55
|
22 |
|
return $this; |
56
|
|
|
} |
57
|
34 |
|
public function __get($property) |
58
|
|
|
{ |
59
|
34 |
|
if (isset($this->changed[$property])) { |
60
|
2 |
|
return $this->changed[$property]; |
61
|
|
|
} |
62
|
34 |
|
if (isset($this->initial[$property])) { |
63
|
34 |
|
return $this->initial[$property]; |
64
|
|
|
} |
65
|
16 |
|
if (isset($this->fetched[$property])) { |
66
|
16 |
|
return is_callable($this->fetched[$property]) ? |
67
|
6 |
|
$this->fetched[$property] = call_user_func($this->fetched[$property]) : |
68
|
16 |
|
$this->fetched[$property]; |
69
|
|
|
} |
70
|
|
|
return null; |
71
|
|
|
} |
72
|
6 |
|
public function __set($property, $value) |
73
|
|
|
{ |
74
|
6 |
|
$this->changed[$property] = $value; |
75
|
6 |
|
} |
76
|
|
|
public function __call($method, $args) |
77
|
|
|
{ |
78
|
|
|
if (isset($this->definition->getRelations()[$method])) { |
79
|
|
|
if (isset($this->fetched[$method])) { |
80
|
|
|
return is_callable($this->fetched[$method]) ? |
81
|
|
|
$this->fetched[$method] = call_user_func($this->fetched[$method], $args[0] ?? null) : |
82
|
|
|
$this->fetched[$method]; |
83
|
|
|
} |
84
|
|
|
} |
85
|
|
|
return null; |
86
|
|
|
} |
87
|
24 |
|
public function definition() |
88
|
|
|
{ |
89
|
24 |
|
return $this->definition; |
90
|
|
|
} |
91
|
6 |
|
public function toArray(bool $fetch = false) |
92
|
|
|
{ |
93
|
6 |
|
$data = []; |
94
|
6 |
|
foreach ($this->definition->getColumns() as $k) { |
95
|
6 |
|
if (isset($this->fetched[$k])) { |
96
|
|
|
if ($fetch) { |
97
|
|
|
$this->fetched[$k] = call_user_func($this->fetched[$k]); |
98
|
|
|
} |
99
|
|
|
if (!is_callable($this->fetched[$k])) { |
100
|
|
|
$data[$k] = $this->fetched[$k]; |
101
|
|
|
} |
102
|
|
|
} |
103
|
6 |
|
if (isset($this->initial[$k])) { |
104
|
4 |
|
$data[$k] = $this->initial[$k]; |
105
|
|
|
} |
106
|
6 |
|
if (isset($this->changed[$k])) { |
107
|
6 |
|
$data[$k] = $this->changed[$k]; |
108
|
|
|
} |
109
|
|
|
} |
110
|
6 |
|
return $data; |
111
|
|
|
} |
112
|
2 |
|
public function fromArray(array $data) |
113
|
|
|
{ |
114
|
2 |
|
foreach ($this->definition->getColumns() as $k) { |
115
|
2 |
|
if (isset($data[$k])) { |
116
|
2 |
|
$this->changed[$k] = $data[$k]; |
117
|
|
|
} |
118
|
|
|
} |
119
|
2 |
|
return $this; |
120
|
|
|
} |
121
|
24 |
|
public function id() |
122
|
|
|
{ |
123
|
24 |
|
$primary = []; |
124
|
24 |
|
foreach ($this->definition->getPrimaryKey() as $k) { |
125
|
24 |
|
$primary[$k] = $this->initial[$k] ?? null; |
126
|
|
|
} |
127
|
24 |
|
return $primary; |
128
|
|
|
} |
129
|
6 |
|
public function save() |
130
|
|
|
{ |
131
|
6 |
|
$this->mapper->save($this); |
132
|
6 |
|
return $this->flatten(); |
133
|
|
|
} |
134
|
2 |
|
public function delete() |
135
|
|
|
{ |
136
|
2 |
|
$this->mapper->delete($this); |
137
|
2 |
|
} |
138
|
|
|
public function refresh() |
139
|
|
|
{ |
140
|
|
|
$this->mapper->refresh($this); |
141
|
|
|
return $this->flatten(); |
142
|
|
|
} |
143
|
6 |
|
public function flatten() |
144
|
|
|
{ |
145
|
6 |
|
$this->initial = $this->toArray(); |
146
|
6 |
|
$this->changed = []; |
147
|
6 |
|
return $this; |
148
|
|
|
} |
149
|
|
|
}; |
150
|
19 |
|
if ($empty) { |
151
|
2 |
|
return $entity; |
152
|
|
|
} |
153
|
17 |
|
$this->lazy($entity, $data); |
154
|
17 |
|
return $this->objects[$definition->getName()][base64_encode(serialize($primary))] = $entity; |
|
|
|
|
155
|
|
|
} |
156
|
|
|
/** |
157
|
|
|
* Get a collection of entities |
158
|
|
|
* |
159
|
|
|
* @param TableQuery $iterator |
160
|
|
|
* @param Table $definition |
161
|
|
|
* @return Collection |
162
|
|
|
*/ |
163
|
36 |
|
public function collection(TableQueryIterator $iterator, Table $definition) : Collection |
164
|
|
|
{ |
165
|
36 |
|
return Collection::from($iterator) |
166
|
36 |
|
->map(function ($v) use ($definition) { |
167
|
36 |
|
return $this->entity($definition, $v); |
168
|
36 |
|
}); |
169
|
|
|
} |
170
|
|
|
/** |
171
|
|
|
* Persist all changes to an entity in the DB. Does not include modified relation collections. |
172
|
|
|
* |
173
|
|
|
* @param object $entity |
174
|
|
|
* @return object |
175
|
|
|
*/ |
176
|
6 |
|
public function save($entity) |
177
|
|
|
{ |
178
|
6 |
|
$query = $this->db->table($entity->definition()->getName()); |
179
|
6 |
|
$primary = $entity->id(); |
180
|
6 |
|
if (!isset($this->objects[$entity->definition()->getName()][base64_encode(serialize($primary))])) { |
181
|
2 |
|
$new = $query->insert($entity->toArray()); |
182
|
2 |
|
$entity->fromArray($new); |
183
|
2 |
|
$this->objects[$entity->definition()->getName()][base64_encode(serialize($new))] = $entity; |
184
|
|
|
} else { |
185
|
4 |
|
foreach ($primary as $k => $v) { |
186
|
4 |
|
$query->filter($k, $v); |
187
|
|
|
} |
188
|
4 |
|
$query->update($entity->toArray()); |
189
|
4 |
|
$new = []; |
190
|
4 |
|
foreach ($primary as $k => $v) { |
191
|
4 |
|
$new[$k] = $entity->{$k}; |
192
|
|
|
} |
193
|
4 |
|
if (base64_encode(serialize($new)) !== base64_encode(serialize($primary))) { |
194
|
2 |
|
unset($this->objects[$entity->definition()->getName()][base64_encode(serialize($primary))]); |
195
|
2 |
|
$this->objects[$entity->definition()->getName()][base64_encode(serialize($new))] = $entity; |
196
|
|
|
} |
197
|
|
|
} |
198
|
6 |
|
return $this->lazy($entity, $entity->toArray()); |
199
|
|
|
} |
200
|
|
|
/** |
201
|
|
|
* Delete an entity from the database |
202
|
|
|
* |
203
|
|
|
* @param object $entity |
204
|
|
|
* @return void |
205
|
|
|
*/ |
206
|
2 |
|
public function delete($entity) |
207
|
|
|
{ |
208
|
2 |
|
$query = $this->db->table($entity->definition()->getName()); |
209
|
2 |
|
$primary = $entity->id(); |
210
|
2 |
|
if (isset($this->objects[$entity->definition()->getName()][base64_encode(serialize($primary))])) { |
211
|
2 |
|
foreach ($primary as $k => $v) { |
212
|
2 |
|
$query->filter($k, $v); |
213
|
|
|
} |
214
|
2 |
|
$query->delete(); |
215
|
2 |
|
unset($this->objects[$entity->definition()->getName()][base64_encode(serialize($primary))]); |
216
|
|
|
} |
217
|
2 |
|
} |
218
|
|
|
/** |
219
|
|
|
* Refresh an entity from the DB (includes own columns and relations). |
220
|
|
|
* |
221
|
|
|
* @param object $entity |
222
|
|
|
* @return object |
223
|
|
|
*/ |
224
|
|
|
public function refresh($entity) |
225
|
|
|
{ |
226
|
|
|
$query = $this->db->table($entity->definition()->getName()); |
227
|
|
|
$primary = $entity->id(); |
228
|
|
|
foreach ($primary as $k => $v) { |
229
|
|
|
$query->filter($k, $v); |
230
|
|
|
} |
231
|
|
|
$data = $query[0] ?? []; |
232
|
|
|
$entity->fromArray($data); |
233
|
|
|
return $this->lazy($entity, $data); |
234
|
|
|
} |
235
|
22 |
|
protected function lazy($entity, $data) |
236
|
|
|
{ |
237
|
22 |
|
$primary = $entity->id(); |
238
|
22 |
|
$definition = $entity->definition(); |
239
|
22 |
|
foreach ($definition->getColumns() as $column) { |
240
|
22 |
|
if (!isset($data[$column])) { |
241
|
22 |
|
$entity->__lazyProperty($column, function () use ($entity, $definition, $primary, $column) { |
242
|
|
|
$query = $this->db->table($definition->getName()); |
243
|
|
|
foreach ($primary as $k => $v) { |
244
|
|
|
$query->filter($k, $v); |
245
|
|
|
} |
246
|
|
|
return $query->select([$column])[0][$column] ?? null; |
247
|
22 |
|
}); |
248
|
|
|
} |
249
|
|
|
} |
250
|
22 |
|
foreach ($definition->getRelations() as $name => $relation) { |
251
|
22 |
|
$entity->__lazyProperty( |
252
|
22 |
|
$name, |
253
|
22 |
|
isset($data[$name]) ? |
254
|
|
|
($relation->many ? |
255
|
|
|
array_map(function ($v) use ($relation) { |
256
|
|
|
return $this->entity($relation->table, $v); |
257
|
|
|
}, $data[$name]) : |
258
|
|
|
$this->entity($relation->table, $data[$name]) |
259
|
|
|
) : |
260
|
22 |
|
function (array $columns = null) use ($entity, $definition, $primary, $relation, $data) { |
261
|
6 |
|
$query = $this->db->table($relation->table->getName(), true); |
|
|
|
|
262
|
6 |
|
if ($columns !== null) { |
263
|
|
|
$query->columns($columns); |
264
|
|
|
} |
265
|
6 |
|
if ($relation->sql) { |
266
|
|
|
$query->where($relation->sql, $relation->par); |
267
|
|
|
} |
268
|
6 |
|
if ($relation->pivot) { |
269
|
4 |
|
$nm = null; |
270
|
4 |
|
foreach ($relation->table->getRelations() as $rname => $rdata) { |
271
|
4 |
|
if ($rdata->pivot && $rdata->pivot->getName() === $relation->pivot->getName()) { |
272
|
4 |
|
$nm = $rname; |
273
|
|
|
} |
274
|
|
|
} |
275
|
4 |
|
if (!$nm) { |
276
|
|
|
$nm = $definition->getName(); |
277
|
|
|
$relation->table->manyToMany( |
278
|
|
|
$this->db->table($definition->getName()), |
279
|
|
|
$relation->pivot, |
280
|
|
|
$nm, |
281
|
|
|
array_flip($relation->keymap), |
282
|
|
|
$relation->pivot_keymap |
283
|
|
|
); |
284
|
|
|
} |
285
|
4 |
|
foreach ($definition->getPrimaryKey() as $v) { |
286
|
4 |
|
$query->filter($nm . '.' . $v, $data[$v] ?? null); |
287
|
|
|
} |
288
|
|
|
} else { |
289
|
6 |
|
foreach ($relation->keymap as $k => $v) { |
290
|
6 |
|
$query->filter($v, $entity->{$k} ?? null); |
291
|
|
|
} |
292
|
|
|
} |
293
|
6 |
|
return $relation->many ? |
294
|
6 |
|
$query->iterator() : |
295
|
6 |
|
$query[0]; |
296
|
22 |
|
} |
297
|
|
|
); |
298
|
|
|
} |
299
|
22 |
|
return $entity; |
300
|
|
|
} |
301
|
|
|
} |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.