1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Analogue\ORM\System; |
4
|
|
|
|
5
|
|
|
use Analogue\ORM\Commands\Delete; |
6
|
|
|
use Analogue\ORM\Commands\Store; |
7
|
|
|
use Analogue\ORM\Drivers\DBAdapter; |
8
|
|
|
use Analogue\ORM\EntityMap; |
9
|
|
|
use Analogue\ORM\Exceptions\MappingException; |
10
|
|
|
use Analogue\ORM\Mappable; |
11
|
|
|
use Analogue\ORM\System\Wrappers\Wrapper; |
12
|
|
|
use Illuminate\Contracts\Events\Dispatcher; |
13
|
|
|
use Illuminate\Support\Collection; |
14
|
|
|
use InvalidArgumentException; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* The mapper provide all the interactions with the database layer |
18
|
|
|
* and holds the states for the loaded entity. One instance is |
19
|
|
|
* created by used entity class during the application lifecycle. |
20
|
|
|
* |
21
|
|
|
* @mixin \Analogue\ORM\System\Query |
22
|
|
|
*/ |
23
|
|
|
class Mapper |
24
|
|
|
{ |
25
|
|
|
/** |
26
|
|
|
* The Manager instance |
27
|
|
|
* |
28
|
|
|
* @var \Analogue\ORM\System\Manager |
29
|
|
|
*/ |
30
|
|
|
protected $manager; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Instance of EntityMapper Object |
34
|
|
|
* |
35
|
|
|
* @var \Analogue\ORM\EntityMap |
36
|
|
|
*/ |
37
|
|
|
protected $entityMap; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* The instance of db adapter |
41
|
|
|
* |
42
|
|
|
* @var \Analogue\ORM\Drivers\DBAdapter |
43
|
|
|
*/ |
44
|
|
|
protected $adapter; |
45
|
|
|
|
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Event dispatcher instance |
49
|
|
|
* |
50
|
|
|
* @var \Illuminate\Contracts\Events\Dispatcher |
51
|
|
|
*/ |
52
|
|
|
protected $dispatcher; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Entity Cache |
56
|
|
|
* |
57
|
|
|
* @var \Analogue\ORM\System\EntityCache |
58
|
|
|
*/ |
59
|
|
|
protected $cache; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Global scopes |
63
|
|
|
* |
64
|
|
|
* @var array |
65
|
|
|
*/ |
66
|
|
|
protected $globalScopes = []; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Custom Commands |
70
|
|
|
* |
71
|
|
|
* @var array |
72
|
|
|
*/ |
73
|
|
|
protected $customCommands = []; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @param EntityMap $entityMap |
77
|
|
|
* @param DBAdapter $adapter |
78
|
|
|
* @param Dispatcher $dispatcher |
79
|
|
|
* @param Manager $manager |
80
|
|
|
*/ |
81
|
|
|
public function __construct(EntityMap $entityMap, DBAdapter $adapter, Dispatcher $dispatcher, Manager $manager) |
82
|
|
|
{ |
83
|
|
|
$this->entityMap = $entityMap; |
84
|
|
|
|
85
|
|
|
$this->adapter = $adapter; |
86
|
|
|
|
87
|
|
|
$this->dispatcher = $dispatcher; |
88
|
|
|
|
89
|
|
|
$this->manager = $manager; |
90
|
|
|
|
91
|
|
|
$this->cache = new EntityCache($entityMap); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Persist an entity or an entity collection into the database |
96
|
|
|
* |
97
|
|
|
* @param Mappable|\Traversable|array $entity |
98
|
|
|
* @throws \InvalidArgumentException |
99
|
|
|
* @throws MappingException |
100
|
|
|
* @return Mappable|\Traversable|array |
101
|
|
|
*/ |
102
|
|
|
public function store($entity) |
103
|
|
|
{ |
104
|
|
|
if ($this->manager->isTraversable($entity)) { |
105
|
|
|
return $this->storeCollection($entity); |
|
|
|
|
106
|
|
|
} else { |
107
|
|
|
return $this->storeEntity($entity); |
|
|
|
|
108
|
|
|
} |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Store an entity collection inside a single DB Transaction |
113
|
|
|
* |
114
|
|
|
* @param \Traversable|array $entities |
115
|
|
|
* @throws \InvalidArgumentException |
116
|
|
|
* @throws MappingException |
117
|
|
|
* @return \Traversable|array |
118
|
|
|
*/ |
119
|
|
|
protected function storeCollection($entities) |
120
|
|
|
{ |
121
|
|
|
$this->adapter->beginTransaction(); |
122
|
|
|
|
123
|
|
|
foreach ($entities as $entity) { |
124
|
|
|
$this->storeEntity($entity); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
$this->adapter->commit(); |
128
|
|
|
|
129
|
|
|
return $entities; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Store a single entity into the database |
134
|
|
|
* |
135
|
|
|
* @param Mappable $entity |
136
|
|
|
* @throws \InvalidArgumentException |
137
|
|
|
* @throws MappingException |
138
|
|
|
* @return \Analogue\ORM\Entity |
139
|
|
|
*/ |
140
|
|
|
protected function storeEntity($entity) |
141
|
|
|
{ |
142
|
|
|
$this->checkEntityType($entity); |
143
|
|
|
|
144
|
|
|
$store = new Store($this->aggregate($entity), $this->newQueryBuilder()); |
145
|
|
|
|
146
|
|
|
return $store->execute(); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Check that the entity correspond to the current mapper. |
151
|
|
|
* |
152
|
|
|
* @param mixed $entity |
153
|
|
|
* @throws InvalidArgumentException |
154
|
|
|
* @return void |
155
|
|
|
*/ |
156
|
|
|
protected function checkEntityType($entity) |
157
|
|
|
{ |
158
|
|
|
if (get_class($entity) != $this->entityMap->getClass()) { |
159
|
|
|
$expected = $this->entityMap->getClass(); |
160
|
|
|
$actual = get_class($entity); |
161
|
|
|
throw new InvalidArgumentException("Expected : $expected, got $actual."); |
162
|
|
|
} |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Convert an entity into an aggregate root |
167
|
|
|
* |
168
|
|
|
* @param mixed $entity |
169
|
|
|
* @throws MappingException |
170
|
|
|
* @return \Analogue\ORM\System\Aggregate |
171
|
|
|
*/ |
172
|
|
|
protected function aggregate($entity) |
173
|
|
|
{ |
174
|
|
|
return new Aggregate($entity); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Get a the Underlying QueryAdapter. |
179
|
|
|
* |
180
|
|
|
* @return \Analogue\ORM\Drivers\QueryAdapter |
181
|
|
|
*/ |
182
|
|
|
public function newQueryBuilder() |
183
|
|
|
{ |
184
|
|
|
return $this->adapter->getQuery(); |
|
|
|
|
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Delete an entity or an entity collection from the database |
189
|
|
|
* |
190
|
|
|
* @param Mappable|\Traversable|array |
191
|
|
|
* @throws MappingException |
192
|
|
|
* @throws \InvalidArgumentException |
193
|
|
|
* @return \Traversable|array |
194
|
|
|
*/ |
195
|
|
|
public function delete($entity) |
196
|
|
|
{ |
197
|
|
|
if ($this->manager->isTraversable($entity)) { |
198
|
|
|
return $this->deleteCollection($entity); |
199
|
|
|
} else { |
200
|
|
|
$this->deleteEntity($entity); |
201
|
|
|
} |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Delete an Entity Collection inside a single db transaction |
206
|
|
|
* |
207
|
|
|
* @param \Traversable|array $entities |
208
|
|
|
* @throws \InvalidArgumentException |
209
|
|
|
* @throws MappingException |
210
|
|
|
* @return \Traversable|array |
211
|
|
|
*/ |
212
|
|
|
protected function deleteCollection($entities) |
213
|
|
|
{ |
214
|
|
|
$this->adapter->beginTransaction(); |
215
|
|
|
|
216
|
|
|
foreach ($entities as $entity) { |
217
|
|
|
$this->deleteEntity($entity); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
$this->adapter->commit(); |
221
|
|
|
|
222
|
|
|
return $entities; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Delete a single entity from the database. |
227
|
|
|
* |
228
|
|
|
* @param Mappable $entity |
229
|
|
|
* @throws \InvalidArgumentException |
230
|
|
|
* @throws MappingException |
231
|
|
|
* @return void |
232
|
|
|
*/ |
233
|
|
|
protected function deleteEntity($entity) |
234
|
|
|
{ |
235
|
|
|
$this->checkEntityType($entity); |
236
|
|
|
|
237
|
|
|
$delete = new Delete($this->aggregate($entity), $this->newQueryBuilder()); |
238
|
|
|
|
239
|
|
|
$delete->execute(); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Return the entity map for this mapper |
244
|
|
|
* |
245
|
|
|
* @return EntityMap |
246
|
|
|
*/ |
247
|
|
|
public function getEntityMap() |
248
|
|
|
{ |
249
|
|
|
return $this->entityMap; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Get the entity cache for the current mapper |
254
|
|
|
* |
255
|
|
|
* @return EntityCache $entityCache |
256
|
|
|
*/ |
257
|
|
|
public function getEntityCache() |
258
|
|
|
{ |
259
|
|
|
return $this->cache; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Fire the given event for the entity |
264
|
|
|
* |
265
|
|
|
* @param string $event |
266
|
|
|
* @param \Analogue\ORM\Entity $entity |
267
|
|
|
* @param bool $halt |
268
|
|
|
* @throws InvalidArgumentException |
269
|
|
|
* @return mixed |
270
|
|
|
*/ |
271
|
|
|
public function fireEvent($event, $entity, $halt = true) |
272
|
|
|
{ |
273
|
|
|
if ($entity instanceof Wrapper) { |
274
|
|
|
throw new InvalidArgumentException('Fired Event with invalid Entity Object'); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
$event = "analogue.{$event}." . $this->entityMap->getClass(); |
278
|
|
|
|
279
|
|
|
$method = $halt ? 'until' : 'fire'; |
280
|
|
|
|
281
|
|
|
return $this->dispatcher->$method($event, $entity); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* Register an entity event with the dispatcher. |
286
|
|
|
* |
287
|
|
|
* @param string $event |
288
|
|
|
* @param \Closure $callback |
289
|
|
|
* @return void |
290
|
|
|
*/ |
291
|
|
|
public function registerEvent($event, $callback) |
292
|
|
|
{ |
293
|
|
|
$name = $this->entityMap->getClass(); |
294
|
|
|
|
295
|
|
|
$this->dispatcher->listen("analogue.{$event}.{$name}", $callback); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* Add a global scope to this mapper query builder |
300
|
|
|
* |
301
|
|
|
* @param ScopeInterface $scope |
302
|
|
|
* @return void |
303
|
|
|
*/ |
304
|
|
|
public function addGlobalScope(ScopeInterface $scope) |
305
|
|
|
{ |
306
|
|
|
$this->globalScopes[get_class($scope)] = $scope; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* Determine if the mapper has a global scope. |
311
|
|
|
* |
312
|
|
|
* @param \Analogue\ORM\System\ScopeInterface $scope |
313
|
|
|
* @return bool |
314
|
|
|
*/ |
315
|
|
|
public function hasGlobalScope($scope) |
316
|
|
|
{ |
317
|
|
|
return !is_null($this->getGlobalScope($scope)); |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* Get a global scope registered with the modal. |
322
|
|
|
* |
323
|
|
|
* @param \Analogue\ORM\System\ScopeInterface $scope |
324
|
|
|
* @return \Analogue\ORM\System\ScopeInterface|null |
325
|
|
|
*/ |
326
|
|
|
public function getGlobalScope($scope) |
327
|
|
|
{ |
328
|
|
|
return array_first($this->globalScopes, function ($key, $value) use ($scope) { |
329
|
|
|
return $scope instanceof $value; |
330
|
|
|
}); |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* Get a new query instance without a given scope. |
335
|
|
|
* |
336
|
|
|
* @param \Analogue\ORM\System\ScopeInterface $scope |
337
|
|
|
* @return \Analogue\ORM\System\Query |
338
|
|
|
*/ |
339
|
|
|
public function newQueryWithoutScope($scope) |
340
|
|
|
{ |
341
|
|
|
$this->getGlobalScope($scope)->remove($query = $this->getQuery(), $this); |
|
|
|
|
342
|
|
|
|
343
|
|
|
return $query; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* Get the Analogue Query Builder for this instance |
348
|
|
|
* |
349
|
|
|
* @return \Analogue\ORM\System\Query |
350
|
|
|
*/ |
351
|
|
|
public function getQuery() |
352
|
|
|
{ |
353
|
|
|
$query = new Query($this, $this->adapter); |
354
|
|
|
|
355
|
|
|
return $this->applyGlobalScopes($query); |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Apply all of the global scopes to an Analogue Query builder. |
360
|
|
|
* |
361
|
|
|
* @param Query $query |
362
|
|
|
* @return \Analogue\ORM\System\Query |
363
|
|
|
*/ |
364
|
|
|
public function applyGlobalScopes($query) |
365
|
|
|
{ |
366
|
|
|
foreach ($this->getGlobalScopes() as $scope) { |
367
|
|
|
$scope->apply($query, $this); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
return $query; |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* Get the global scopes for this class instance. |
375
|
|
|
* |
376
|
|
|
* @return \Analogue\ORM\System\ScopeInterface |
377
|
|
|
*/ |
378
|
|
|
public function getGlobalScopes() |
379
|
|
|
{ |
380
|
|
|
return $this->globalScopes; |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
/** |
384
|
|
|
* Add a dynamic method that extends the mapper/repository |
385
|
|
|
* |
386
|
|
|
* @param string $command |
387
|
|
|
*/ |
388
|
|
|
public function addCustomCommand($command) |
389
|
|
|
{ |
390
|
|
|
$name = lcfirst(class_basename($command)); |
391
|
|
|
|
392
|
|
|
$this->customCommands[$name] = $command; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
/** |
396
|
|
|
* Create a new instance of the mapped entity class |
397
|
|
|
* |
398
|
|
|
* @param array $attributes |
399
|
|
|
* @return mixed |
400
|
|
|
*/ |
401
|
|
|
public function newInstance($attributes = []) |
402
|
|
|
{ |
403
|
|
|
$class = $this->entityMap->getClass(); |
404
|
|
|
|
405
|
|
|
if ($this->entityMap->activator() != null) { |
406
|
|
|
$entity = $this->entityMap->activator(); |
|
|
|
|
407
|
|
|
} else { |
408
|
|
|
$entity = $this->customClassInstance($class); |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
// prevent hydrating with an empty array |
412
|
|
|
if (count($attributes) > 0) { |
413
|
|
|
$entity->setEntityAttributes($attributes); |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
return $entity; |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
/** |
420
|
|
|
* Use a trick to generate a class prototype that we |
421
|
|
|
* can instantiate without calling the constructor. |
422
|
|
|
* |
423
|
|
|
* @param string|null $className |
424
|
|
|
* @throws MappingException |
425
|
|
|
* @return mixed |
426
|
|
|
*/ |
427
|
|
|
protected function customClassInstance($className) |
428
|
|
|
{ |
429
|
|
|
if (!class_exists($className)) { |
430
|
|
|
throw new MappingException("Tried to instantiate a non-existing Entity class : $className"); |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
$prototype = unserialize(sprintf('O:%d:"%s":0:{}', strlen($className), $className)); |
434
|
|
|
|
435
|
|
|
return $prototype; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
/** |
439
|
|
|
* Get an unscoped Analogue Query Builder for this instance |
440
|
|
|
* |
441
|
|
|
* @return \Analogue\ORM\System\Query |
442
|
|
|
*/ |
443
|
|
|
public function globalQuery() |
444
|
|
|
{ |
445
|
|
|
return $this->newQueryWithoutScopes(); |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
/** |
449
|
|
|
* Get a new query builder that doesn't have any global scopes. |
450
|
|
|
* |
451
|
|
|
* @return Query |
452
|
|
|
*/ |
453
|
|
|
public function newQueryWithoutScopes() |
454
|
|
|
{ |
455
|
|
|
return $this->removeGlobalScopes($this->getQuery()); |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
/** |
459
|
|
|
* Remove all of the global scopes from an Analogue Query builder. |
460
|
|
|
* |
461
|
|
|
* @param Query $query |
462
|
|
|
* @return \Analogue\ORM\System\Query |
463
|
|
|
*/ |
464
|
|
|
public function removeGlobalScopes($query) |
465
|
|
|
{ |
466
|
|
|
foreach ($this->getGlobalScopes() as $scope) { |
467
|
|
|
$scope->remove($query, $this); |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
return $query; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
/** |
474
|
|
|
* Return the manager instance |
475
|
|
|
* |
476
|
|
|
* @return \Analogue\ORM\System\Manager |
477
|
|
|
*/ |
478
|
|
|
public function getManager() |
479
|
|
|
{ |
480
|
|
|
return $this->manager; |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
/** |
484
|
|
|
* Dynamically handle calls to custom commands, or Redirects to query() |
485
|
|
|
* |
486
|
|
|
* @param string $method |
487
|
|
|
* @param array $parameters |
488
|
|
|
* @throws \Exception |
489
|
|
|
* @return mixed |
490
|
|
|
*/ |
491
|
|
|
public function __call($method, $parameters) |
492
|
|
|
{ |
493
|
|
|
// Check if method is a custom command on the mapper |
494
|
|
|
if ($this->hasCustomCommand($method)) { |
495
|
|
|
if (count($parameters) == 0) { |
496
|
|
|
throw new \Exception("$method must at least have 1 argument"); |
497
|
|
|
} |
498
|
|
|
|
499
|
|
|
return $this->executeCustomCommand($method, $parameters[0]); |
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
// Redirect call on a new query instance |
503
|
|
|
return call_user_func_array([$this->query(), $method], $parameters); |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
/** |
507
|
|
|
* Check if this mapper supports this command |
508
|
|
|
* @param string $command |
509
|
|
|
* @return boolean |
510
|
|
|
*/ |
511
|
|
|
public function hasCustomCommand($command) |
512
|
|
|
{ |
513
|
|
|
return in_array($command, $this->getCustomCommands()); |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
/** |
517
|
|
|
* Get all the custom commands registered on this mapper |
518
|
|
|
* |
519
|
|
|
* @return array |
520
|
|
|
*/ |
521
|
|
|
public function getCustomCommands() |
522
|
|
|
{ |
523
|
|
|
return array_keys($this->customCommands); |
524
|
|
|
} |
525
|
|
|
|
526
|
|
|
/** |
527
|
|
|
* Execute a custom command on an Entity |
528
|
|
|
* |
529
|
|
|
* @param string $command |
530
|
|
|
* @param mixed|Collection|array $entity |
531
|
|
|
* @throws \InvalidArgumentException |
532
|
|
|
* @throws MappingException |
533
|
|
|
* @return mixed |
534
|
|
|
*/ |
535
|
|
|
public function executeCustomCommand($command, $entity) |
536
|
|
|
{ |
537
|
|
|
$commandClass = $this->customCommands[$command]; |
538
|
|
|
|
539
|
|
|
if ($this->manager->isTraversable($entity)) { |
540
|
|
|
foreach ($entity as $instance) { |
541
|
|
|
$this->executeSingleCustomCommand($commandClass, $instance); |
542
|
|
|
} |
543
|
|
|
} else { |
544
|
|
|
return $this->executeSingleCustomCommand($commandClass, $entity); |
545
|
|
|
} |
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
/** |
549
|
|
|
* Execute a single command instance |
550
|
|
|
* |
551
|
|
|
* @param string $commandClass |
552
|
|
|
* @param mixed $entity |
553
|
|
|
* @throws \InvalidArgumentException |
554
|
|
|
* @throws MappingException |
555
|
|
|
* @return mixed |
556
|
|
|
*/ |
557
|
|
|
protected function executeSingleCustomCommand($commandClass, $entity) |
558
|
|
|
{ |
559
|
|
|
$this->checkEntityType($entity); |
560
|
|
|
|
561
|
|
|
$instance = new $commandClass($this->aggregate($entity), $this->newQueryBuilder()); |
562
|
|
|
|
563
|
|
|
return $instance->execute(); |
564
|
|
|
} |
565
|
|
|
|
566
|
|
|
/** |
567
|
|
|
* Get the Analogue Query Builder for this instance |
568
|
|
|
* |
569
|
|
|
* @return \Analogue\ORM\System\Query |
570
|
|
|
*/ |
571
|
|
|
public function query() |
572
|
|
|
{ |
573
|
|
|
return $this->getQuery(); |
574
|
|
|
} |
575
|
|
|
} |
576
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.