1
|
|
|
<?php |
|
|
|
|
2
|
|
|
|
3
|
|
|
namespace Respect\Structural; |
4
|
|
|
|
5
|
|
|
use Exception; |
6
|
|
|
use Respect\Data\AbstractMapper; |
7
|
|
|
use Respect\Data\CollectionIterator; |
8
|
|
|
use Respect\Data\Collections as c; |
9
|
|
|
use Respect\Data\Collections\Collection; |
10
|
|
|
use SplObjectStorage; |
11
|
|
|
|
12
|
|
|
/** Maps objects to nosql operations */ |
13
|
|
|
class Mapper extends AbstractMapper implements |
|
|
|
|
14
|
|
|
c\Filterable, |
15
|
|
|
c\Mixable, |
16
|
|
|
c\Typable |
17
|
|
|
{ |
18
|
|
|
/** @var \Respect\Structural\Driver Holds our connector* */ |
19
|
|
|
protected $driver; |
20
|
|
|
|
21
|
|
|
/** @var string Namespace to look for entities * */ |
22
|
|
|
public $entityNamespace = '\\'; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @param Driver $driver |
26
|
|
|
*/ |
27
|
3 |
|
public function __construct(Driver $driver) |
28
|
|
|
{ |
29
|
3 |
|
parent::__construct(); |
30
|
|
|
|
31
|
3 |
|
$this->driver = $driver; |
32
|
3 |
|
} |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @return Mapper |
36
|
|
|
*/ |
37
|
3 |
|
public function __get($name) |
38
|
|
|
{ |
39
|
3 |
|
return parent::__get($name); |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Flushes a single instance into the database. This method supports |
44
|
|
|
* mixing, so flushing a mixed instance will flush distinct tables on the |
45
|
|
|
* database |
46
|
|
|
* |
47
|
|
|
* @param object $entity Entity instance to be flushed |
48
|
|
|
* |
49
|
|
|
* @return null |
50
|
|
|
*/ |
51
|
3 |
|
protected function flushSingle($entity) |
52
|
|
|
{ |
53
|
3 |
|
$coll = $this->tracked[$entity]; |
54
|
3 |
|
$cols = $this->extractColumns($entity, $coll); |
55
|
|
|
|
56
|
3 |
|
if ($this->removed->contains($entity)) { |
57
|
1 |
|
$this->rawDelete($coll, $entity); |
58
|
3 |
|
} elseif ($this->new->contains($entity)) { |
59
|
1 |
|
$this->rawInsert($coll, $entity); |
60
|
1 |
|
} else { |
61
|
1 |
|
$this->rawUpdate($cols, $coll); |
62
|
|
|
} |
63
|
3 |
|
} |
64
|
|
|
|
65
|
2 |
|
public function persist($object, Collection $onCollection) |
66
|
|
|
{ |
67
|
2 |
|
$next = $onCollection->getNext(); |
68
|
|
|
|
69
|
2 |
|
if ($this->filterable($onCollection)) { |
70
|
|
|
$next->setMapper($this); |
71
|
|
|
$next->persist($object); |
72
|
|
|
|
73
|
|
|
return; |
74
|
|
|
} |
75
|
|
|
|
76
|
2 |
|
if ($next) { |
77
|
|
|
$remote = $this->getStyle()->remoteIdentifier($next->getName()); |
78
|
|
|
$next->setMapper($this); |
79
|
|
|
$next->persist($object->$remote); |
80
|
|
|
} |
81
|
|
|
|
82
|
2 |
|
foreach ($onCollection->getChildren() as $child) { |
83
|
|
|
$remote = $this->getStyle()->remoteIdentifier($child->getName()); |
84
|
|
|
$child->persist($object->$remote); |
85
|
2 |
|
} |
86
|
|
|
|
87
|
2 |
|
return parent::persist($object, $onCollection); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Receives columns from an entity and her collection. Returns the columns |
92
|
|
|
* that belong only to the main entity. This method supports mixing, so |
93
|
|
|
* extracting mixins will also persist them on their respective |
94
|
|
|
* tables |
95
|
|
|
* |
96
|
|
|
* @param \Respect\Data\Collections\Collection $collection Target collection |
97
|
|
|
* @param array $cols Entity columns |
98
|
|
|
* |
99
|
|
|
* @return array Columns left for the main collection |
100
|
|
|
*/ |
101
|
1 |
|
protected function extractAndOperateMixins(Collection $collection, $cols) |
102
|
|
|
{ |
103
|
1 |
|
if (!$this->mixable($collection)) { |
104
|
1 |
|
return $cols; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
foreach ($this->getMixins($collection) as $mix => $spec) { |
108
|
|
|
//Extract from $cols only the columns from the mixin |
109
|
|
|
$mixCols = array_intersect_key( |
110
|
|
|
$cols, |
111
|
|
|
array_combine(//create array with keys only |
112
|
|
|
$spec, |
113
|
|
|
array_fill(0, count($spec), '') |
114
|
|
|
) |
115
|
|
|
); |
116
|
|
|
if (isset($cols["{$mix}_id"])) { |
117
|
|
|
$mixCols['id'] = $cols["{$mix}_id"]; |
118
|
|
|
$cols = array_diff($cols, $mixCols); //Remove mixin columns |
119
|
|
|
$this->rawUpdate($mixCols, $this->__get($mix)); |
|
|
|
|
120
|
|
|
} else { |
121
|
|
|
$mixCols['id'] = null; |
122
|
|
|
$cols = array_diff($cols, $mixCols); //Remove mixin columns |
123
|
|
|
$this->rawinsert($mixCols, $this->__get($mix)); |
|
|
|
|
124
|
|
|
} |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
return $cols; |
128
|
|
|
} |
129
|
|
|
|
130
|
2 |
|
protected function guessCondition(&$columns, Collection $collection) |
131
|
|
|
{ |
132
|
2 |
|
$primaryName = $this->getStyle()->identifier($collection->getName()); |
133
|
2 |
|
$condition = [$primaryName => $columns[$primaryName]]; |
134
|
2 |
|
unset($columns[$primaryName]); |
135
|
|
|
|
136
|
2 |
|
return $condition; |
137
|
|
|
} |
138
|
|
|
|
139
|
1 |
|
protected function rawDelete(Collection $collection, $entity) |
140
|
|
|
{ |
141
|
1 |
|
$name = $collection->getName(); |
142
|
1 |
|
$columns = $this->extractColumns($entity, $collection); |
143
|
1 |
|
$condition = $this->guessCondition($columns, $collection); |
144
|
|
|
|
145
|
1 |
|
return $this->driver->remove($name, $condition); |
146
|
|
|
} |
147
|
|
|
|
148
|
1 |
|
protected function rawUpdate(array $columns, Collection $collection) |
149
|
|
|
{ |
150
|
1 |
|
$columns = $this->extractAndOperateMixins($collection, $columns); |
151
|
1 |
|
$name = $collection->getName(); |
152
|
1 |
|
$condition = $this->guessCondition($columns, $collection); |
153
|
|
|
|
154
|
1 |
|
$this->driver->update($name, $condition, $columns); |
155
|
1 |
|
} |
156
|
|
|
|
157
|
1 |
|
protected function rawInsert(Collection $collection, $entity = null) |
158
|
|
|
{ |
159
|
1 |
|
$name = $collection->getName(); |
160
|
1 |
|
$this->driver->insert($name, $entity); |
161
|
1 |
|
} |
162
|
|
|
|
163
|
3 |
|
public function flush() |
164
|
|
|
{ |
165
|
|
|
try { |
166
|
3 |
|
foreach ($this->changed as $entity) { |
167
|
3 |
|
$this->flushSingle($entity); |
168
|
3 |
|
} |
169
|
3 |
|
} catch (Exception $e) { |
170
|
|
|
throw $e; |
171
|
|
|
} |
172
|
|
|
|
173
|
3 |
|
$this->reset(); |
174
|
3 |
|
} |
175
|
|
|
|
176
|
2 |
|
protected function createStatement(Collection $collection, $withExtra = null) |
177
|
|
|
{ |
178
|
2 |
|
$query = $this->generateQuery($collection); |
179
|
|
|
|
180
|
2 |
|
$withExtraList = (array)$withExtra; |
181
|
|
|
|
182
|
2 |
|
$withExtraList = array_merge($withExtraList, $query); |
183
|
|
|
|
184
|
2 |
|
return $this->driver->find($collection->getName(), $withExtraList); |
185
|
|
|
} |
186
|
|
|
|
187
|
2 |
|
protected function generateQuery(Collection $collection) |
188
|
|
|
{ |
189
|
2 |
|
return $this->driver->generateQuery($collection); |
190
|
|
|
} |
191
|
|
|
|
192
|
3 |
|
protected function extractColumns($entity, Collection $collection) |
|
|
|
|
193
|
|
|
{ |
194
|
3 |
|
$cols = get_object_vars($entity); |
195
|
|
|
|
196
|
3 |
|
return $cols; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
protected function hasComposition($entity, $next, $parent) |
200
|
|
|
{ |
201
|
|
|
$style = $this->getStyle(); |
202
|
|
|
|
203
|
|
|
return $entity === $style->composed($parent, $next) || $entity === $style->composed($next, $parent); |
204
|
|
|
} |
205
|
|
|
|
206
|
2 |
|
protected function fetchSingle(Collection $collection, $statement) |
207
|
|
|
{ |
208
|
2 |
|
$name = $collection->getName(); |
209
|
2 |
|
$entityName = $name; |
210
|
2 |
|
$row = $this->driver->fetch($statement); |
211
|
|
|
|
212
|
2 |
|
if (!$row) { |
|
|
|
|
213
|
|
|
return false; |
214
|
|
|
} |
215
|
|
|
|
216
|
2 |
|
if ($this->typable($collection)) { |
217
|
|
|
$entityName = $row->{$this->getType($collection)}; |
218
|
|
|
} |
219
|
|
|
|
220
|
2 |
|
$entities = new SplObjectStorage(); |
221
|
2 |
|
$entities[$this->transformSingleRow($row, $entityName)] = $collection; |
222
|
|
|
|
223
|
2 |
|
return $entities; |
224
|
|
|
} |
225
|
|
|
|
226
|
2 |
|
protected function getNewEntityByName($entityName) |
227
|
|
|
{ |
228
|
2 |
|
$entityName = $this->getStyle()->styledName($entityName); |
229
|
2 |
|
$entityClass = $this->entityNamespace . $entityName; |
230
|
2 |
|
$entityClass = class_exists($entityClass) ? $entityClass : '\stdClass'; |
231
|
|
|
|
232
|
2 |
|
return new $entityClass; |
233
|
|
|
} |
234
|
|
|
|
235
|
2 |
|
protected function transformSingleRow($row, $entityName) |
236
|
|
|
{ |
237
|
2 |
|
$newRow = $this->getNewEntityByName($entityName); |
238
|
|
|
|
239
|
2 |
|
foreach ($row as $prop => $value) { |
240
|
2 |
|
$this->inferSet($newRow, $prop, $value); |
241
|
2 |
|
} |
242
|
|
|
|
243
|
2 |
|
return $newRow; |
244
|
|
|
} |
245
|
|
|
|
246
|
2 |
|
protected function inferSet(&$entity, $prop, $value) |
247
|
|
|
{ |
248
|
|
|
try { |
249
|
2 |
|
$mirror = new \ReflectionProperty($entity, $prop); |
250
|
|
|
$mirror->setAccessible(true); |
251
|
|
|
$mirror->setValue($entity, $value); |
252
|
2 |
|
} catch (\ReflectionException $e) { |
253
|
2 |
|
$entity->$prop = $value; |
254
|
|
|
} |
255
|
2 |
|
} |
256
|
|
|
|
257
|
|
|
protected function fetchMulti(Collection $collection, $statement) |
258
|
|
|
{ |
259
|
|
|
$row = $this->driver->fetch($statement); |
260
|
|
|
|
261
|
|
|
if (!$row) { |
|
|
|
|
262
|
|
|
return false; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
$this->postHydrate( |
266
|
|
|
$entities = $this->createEntities($row, $statement, $collection) |
267
|
|
|
); |
268
|
|
|
|
269
|
|
|
return $entities; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
protected function createEntities($row, $statement, Collection $collection) |
273
|
|
|
{ |
274
|
|
|
$entities = new SplObjectStorage(); |
275
|
|
|
$entitiesInstances = $this->buildEntitiesInstances($collection, $entities); |
276
|
|
|
$entityInstance = array_pop($entitiesInstances); |
277
|
|
|
|
278
|
|
|
//Reversely traverses the columns to avoid conflicting foreign key names |
279
|
|
|
foreach (array_reverse($row, true) as $col => $value) { |
280
|
|
|
$columnMeta = $statement->getColumnMeta($col); |
281
|
|
|
$columnName = $columnMeta['name']; |
282
|
|
|
$primaryName = $this->getStyle()->identifier( |
283
|
|
|
$entities[$entityInstance]->getName() |
284
|
|
|
); |
285
|
|
|
|
286
|
|
|
$this->inferSet($entityInstance, $columnName, $value); |
287
|
|
|
|
288
|
|
|
if ($primaryName == $columnName) { |
289
|
|
|
$entityInstance = array_pop($entitiesInstances); |
290
|
|
|
} |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
return $entities; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
protected function buildEntitiesInstances(Collection $collection, SplObjectStorage $entities) |
297
|
|
|
{ |
298
|
|
|
$entitiesInstances = []; |
299
|
|
|
|
300
|
|
|
foreach (CollectionIterator::recursive($collection) as $c) { |
301
|
|
|
if ($this->filterable($c) && !$this->getFilters($c)) { |
302
|
|
|
continue; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
$entityInstance = $this->getNewEntityByName($c->getName()); |
306
|
|
|
|
307
|
|
|
if ($this->mixable($c)) { |
308
|
|
|
$mixins = $this->getMixins($c); |
309
|
|
|
foreach ($mixins as $mix) { |
310
|
|
|
$entitiesInstances[] = $entityInstance; |
311
|
|
|
} |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
$entities[$entityInstance] = $c; |
315
|
|
|
$entitiesInstances[] = $entityInstance; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
return $entitiesInstances; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
protected function postHydrate(SplObjectStorage $entities) |
322
|
|
|
{ |
323
|
|
|
$entitiesClone = clone $entities; |
324
|
|
|
|
325
|
|
|
foreach ($entities as $instance) { |
326
|
|
|
foreach ($instance as $field => &$value) { |
327
|
|
|
if ($this->getStyle()->isRemoteIdentifier($field)) { |
328
|
|
|
foreach ($entitiesClone as $sub) { |
329
|
|
|
$this->tryHydration($entities, $sub, $field, $value); |
330
|
|
|
} |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
protected function tryHydration($entities, $sub, $field, &$value) |
337
|
|
|
{ |
338
|
|
|
$tableName = $entities[$sub]->getName(); |
339
|
|
|
$primaryName = $this->getStyle()->identifier($tableName); |
340
|
|
|
|
341
|
|
|
if ($tableName === $this->getStyle()->remoteFromIdentifier($field) |
342
|
|
|
&& $sub->{$primaryName} === $value |
343
|
|
|
) { |
344
|
|
|
$value = $sub; |
345
|
|
|
} |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
protected function getSetterStyle($name) |
349
|
|
|
{ |
350
|
|
|
$name = str_replace('_', '', $this->getStyle()->styledProperty($name)); |
351
|
|
|
|
352
|
|
|
return "set{$name}"; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
public function getFilters(Collection $collection) |
356
|
|
|
{ |
357
|
|
|
return $collection->getExtra('filters'); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
public function getMixins(Collection $collection) |
361
|
|
|
{ |
362
|
|
|
return $collection->getExtra('mixins'); |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
public function getType(Collection $collection) |
366
|
|
|
{ |
367
|
|
|
return $collection->getExtra('type'); |
368
|
|
|
} |
369
|
|
|
|
370
|
1 |
|
public function mixable(Collection $collection) |
371
|
|
|
{ |
372
|
1 |
|
return $collection->have('mixins'); |
373
|
|
|
} |
374
|
|
|
|
375
|
2 |
|
public function typable(Collection $collection) |
376
|
|
|
{ |
377
|
2 |
|
return $collection->have('type'); |
378
|
|
|
} |
379
|
|
|
|
380
|
2 |
|
public function filterable(Collection $collection) |
381
|
|
|
{ |
382
|
2 |
|
return $collection->have('filters'); |
383
|
|
|
} |
384
|
|
|
} |
|
|
|
|
385
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.