1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Bankiru\Api\Doctrine; |
4
|
|
|
|
5
|
|
|
use Bankiru\Api\Doctrine\Exception\MappingException; |
6
|
|
|
use Bankiru\Api\Doctrine\Hydration\EntityHydrator; |
7
|
|
|
use Bankiru\Api\Doctrine\Mapping\ApiMetadata; |
8
|
|
|
use Bankiru\Api\Doctrine\Mapping\EntityMetadata; |
9
|
|
|
use Bankiru\Api\Doctrine\Persister\ApiPersister; |
10
|
|
|
use Bankiru\Api\Doctrine\Persister\EntityPersister; |
11
|
|
|
use Bankiru\Api\Doctrine\Utility\IdentifierFlattener; |
12
|
|
|
use Doctrine\Common\NotifyPropertyChanged; |
13
|
|
|
use Doctrine\Common\Persistence\ObjectManagerAware; |
14
|
|
|
use Doctrine\Common\PropertyChangedListener; |
15
|
|
|
use Doctrine\Common\Proxy\Proxy; |
16
|
|
|
|
17
|
|
|
class UnitOfWork implements PropertyChangedListener |
18
|
|
|
{ |
19
|
|
|
/** |
20
|
|
|
* An entity is in MANAGED state when its persistence is managed by an EntityManager. |
21
|
|
|
*/ |
22
|
|
|
const STATE_MANAGED = 1; |
23
|
|
|
/** |
24
|
|
|
* An entity is new if it has just been instantiated (i.e. using the "new" operator) |
25
|
|
|
* and is not (yet) managed by an EntityManager. |
26
|
|
|
*/ |
27
|
|
|
const STATE_NEW = 2; |
28
|
|
|
/** |
29
|
|
|
* A detached entity is an instance with persistent state and identity that is not |
30
|
|
|
* (or no longer) associated with an EntityManager (and a UnitOfWork). |
31
|
|
|
*/ |
32
|
|
|
const STATE_DETACHED = 3; |
33
|
|
|
/** |
34
|
|
|
* A removed entity instance is an instance with a persistent identity, |
35
|
|
|
* associated with an EntityManager, whose persistent state will be deleted |
36
|
|
|
* on commit. |
37
|
|
|
*/ |
38
|
|
|
const STATE_REMOVED = 4; |
39
|
|
|
|
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* The (cached) states of any known entities. |
43
|
|
|
* Keys are object ids (spl_object_hash). |
44
|
|
|
* |
45
|
|
|
* @var array |
46
|
|
|
*/ |
47
|
|
|
private $entityStates = []; |
48
|
|
|
|
49
|
|
|
|
50
|
|
|
/** @var EntityManager */ |
51
|
|
|
private $manager; |
52
|
|
|
/** @var EntityPersister[] */ |
53
|
|
|
private $persisters = []; |
54
|
|
|
/** @var array */ |
55
|
|
|
private $entityIdentifiers; |
56
|
|
|
/** @var object[][] */ |
57
|
|
|
private $identityMap; |
58
|
|
|
/** @var IdentifierFlattener */ |
59
|
|
|
private $identifierFlattener; |
60
|
|
|
/** @var array */ |
61
|
|
|
private $originalEntityData; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* UnitOfWork constructor. |
65
|
|
|
* |
66
|
|
|
* @param EntityManager $manager |
67
|
|
|
*/ |
68
|
14 |
|
public function __construct(EntityManager $manager) |
69
|
|
|
{ |
70
|
14 |
|
$this->manager = $manager; |
71
|
14 |
|
$this->identifierFlattener = new IdentifierFlattener($this->manager); |
72
|
14 |
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @param $className |
76
|
|
|
* |
77
|
|
|
* @return EntityPersister |
78
|
|
|
*/ |
79
|
13 |
|
public function getEntityPersister($className) |
80
|
|
|
{ |
81
|
13 |
|
if (!array_key_exists($className, $this->persisters)) { |
82
|
|
|
/** @var ApiMetadata $classMetadata */ |
83
|
13 |
|
$classMetadata = $this->manager->getClassMetadata($className); |
84
|
13 |
|
$this->persisters[$className] = new ApiPersister($this->manager, $classMetadata); |
85
|
13 |
|
} |
86
|
|
|
|
87
|
13 |
|
return $this->persisters[$className]; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Checks whether an entity is registered in the identity map of this UnitOfWork. |
92
|
|
|
* |
93
|
|
|
* @param object $entity |
94
|
|
|
* |
95
|
|
|
* @return boolean |
96
|
|
|
*/ |
97
|
|
|
public function isInIdentityMap($entity) |
98
|
|
|
{ |
99
|
|
|
$oid = spl_object_hash($entity); |
100
|
|
|
|
101
|
|
|
if (!isset($this->entityIdentifiers[$oid])) { |
102
|
|
|
return false; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** @var EntityMetadata $classMetadata */ |
106
|
|
|
$classMetadata = $this->manager->getClassMetadata(get_class($entity)); |
107
|
|
|
$idHash = implode(' ', $this->entityIdentifiers[$oid]); |
108
|
|
|
|
109
|
|
|
if ($idHash === '') { |
110
|
|
|
return false; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Gets the identifier of an entity. |
118
|
|
|
* The returned value is always an array of identifier values. If the entity |
119
|
|
|
* has a composite identifier then the identifier values are in the same |
120
|
|
|
* order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames(). |
121
|
|
|
* |
122
|
|
|
* @param object $entity |
123
|
|
|
* |
124
|
|
|
* @return array The identifier values. |
125
|
|
|
*/ |
126
|
|
|
public function getEntityIdentifier($entity) |
127
|
|
|
{ |
128
|
|
|
return $this->entityIdentifiers[spl_object_hash($entity)]; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* @param $className |
133
|
|
|
* @param \stdClass $data |
134
|
|
|
* @param object|null $unmanagedProxy |
135
|
|
|
* |
136
|
|
|
* @return ObjectManagerAware|object |
137
|
|
|
* @throws MappingException |
138
|
|
|
*/ |
139
|
12 |
|
public function getOrCreateEntity($className, \stdClass $data, $unmanagedProxy = null) |
140
|
|
|
{ |
141
|
|
|
/** @var EntityMetadata $class */ |
142
|
12 |
|
$class = $this->manager->getClassMetadata($className); |
143
|
12 |
|
$hydrator = new EntityHydrator($this->manager, $class); |
144
|
|
|
|
145
|
12 |
|
$tmpEntity = $hydrator->hydarate($data); |
146
|
|
|
|
147
|
12 |
|
$id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($tmpEntity)); |
148
|
12 |
|
$idHash = implode(' ', $id); |
149
|
|
|
|
150
|
12 |
|
$overrideLocalValues = false; |
151
|
12 |
|
if (isset($this->identityMap[$class->rootEntityName][$idHash])) { |
152
|
2 |
|
$entity = $this->identityMap[$class->rootEntityName][$idHash]; |
153
|
2 |
|
$oid = spl_object_hash($entity); |
154
|
|
|
|
155
|
2 |
|
if ($entity instanceof Proxy && !$entity->__isInitialized()) { |
156
|
2 |
|
$entity->__setInitialized(true); |
157
|
|
|
|
158
|
2 |
|
$overrideLocalValues = true; |
159
|
2 |
|
$this->originalEntityData[$oid] = $data; |
160
|
|
|
|
161
|
2 |
|
if ($entity instanceof NotifyPropertyChanged) { |
162
|
|
|
$entity->addPropertyChangedListener($this); |
163
|
|
|
} |
164
|
2 |
|
} |
165
|
2 |
|
} else { |
166
|
11 |
|
$entity = $unmanagedProxy; |
167
|
11 |
|
if (null === $entity) { |
168
|
11 |
|
$entity = $this->newInstance($class); |
169
|
11 |
|
} |
170
|
11 |
|
$this->registerManaged($entity, $id, $data); |
171
|
11 |
|
$overrideLocalValues = true; |
172
|
|
|
} |
173
|
|
|
|
174
|
12 |
|
if (!$overrideLocalValues) { |
175
|
|
|
return $entity; |
176
|
|
|
} |
177
|
|
|
|
178
|
12 |
|
$entity = $hydrator->hydarate($data, $entity); |
179
|
|
|
|
180
|
12 |
|
return $entity; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* @param ApiMetadata $class |
186
|
|
|
* |
187
|
|
|
* @return \Doctrine\Common\Persistence\ObjectManagerAware|object |
188
|
|
|
*/ |
189
|
11 |
|
private function newInstance(ApiMetadata $class) |
190
|
|
|
{ |
191
|
11 |
|
$entity = $class->newInstance(); |
192
|
|
|
|
193
|
11 |
|
if ($entity instanceof ObjectManagerAware) { |
194
|
|
|
$entity->injectObjectManager($this->manager, $class); |
195
|
|
|
} |
196
|
|
|
|
197
|
11 |
|
return $entity; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* INTERNAL: |
202
|
|
|
* Registers an entity as managed. |
203
|
|
|
* |
204
|
|
|
* @param object $entity The entity. |
205
|
|
|
* @param array $id The identifier values. |
206
|
|
|
* @param \stdClass|null $data The original entity data. |
207
|
|
|
* |
208
|
|
|
* @return void |
209
|
|
|
*/ |
210
|
12 |
|
public function registerManaged($entity, array $id, \stdClass $data = null) |
211
|
|
|
{ |
212
|
12 |
|
$oid = spl_object_hash($entity); |
213
|
|
|
|
214
|
12 |
|
$this->entityIdentifiers[$oid] = $id; |
215
|
12 |
|
$this->entityStates[$oid] = self::STATE_MANAGED; |
216
|
12 |
|
$this->originalEntityData[$oid] = $data; |
217
|
|
|
|
218
|
12 |
|
$this->addToIdentityMap($entity); |
219
|
|
|
|
220
|
12 |
|
if ($entity instanceof NotifyPropertyChanged && (!$entity instanceof Proxy || $entity->__isInitialized())) { |
221
|
|
|
$entity->addPropertyChangedListener($this); |
222
|
|
|
} |
223
|
12 |
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* INTERNAL: |
227
|
|
|
* Registers an entity in the identity map. |
228
|
|
|
* Note that entities in a hierarchy are registered with the class name of |
229
|
|
|
* the root entity. |
230
|
|
|
* |
231
|
|
|
* @ignore |
232
|
|
|
* |
233
|
|
|
* @param object $entity The entity to register. |
234
|
|
|
* |
235
|
|
|
* @return boolean TRUE if the registration was successful, FALSE if the identity of |
236
|
|
|
* the entity in question is already managed. |
237
|
|
|
* |
238
|
|
|
*/ |
239
|
12 |
|
public function addToIdentityMap($entity) |
240
|
|
|
{ |
241
|
|
|
/** @var EntityMetadata $classMetadata */ |
242
|
12 |
|
$classMetadata = $this->manager->getClassMetadata(get_class($entity)); |
243
|
12 |
|
$idHash = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]); |
244
|
|
|
|
245
|
12 |
|
if ($idHash === '') { |
246
|
|
|
throw new \InvalidArgumentException('Entitty does not have valid identifiers to be stored at identity map'); |
247
|
|
|
} |
248
|
|
|
|
249
|
12 |
|
$className = $classMetadata->rootEntityName; |
250
|
|
|
|
251
|
12 |
|
if (isset($this->identityMap[$className][$idHash])) { |
252
|
|
|
return false; |
253
|
|
|
} |
254
|
|
|
|
255
|
12 |
|
$this->identityMap[$className][$idHash] = $entity; |
256
|
|
|
|
257
|
12 |
|
return true; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* Gets the identity map of the UnitOfWork. |
262
|
|
|
* |
263
|
|
|
* @return array |
264
|
|
|
*/ |
265
|
|
|
public function getIdentityMap() |
266
|
|
|
{ |
267
|
|
|
return $this->identityMap; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* Gets the original data of an entity. The original data is the data that was |
272
|
|
|
* present at the time the entity was reconstituted from the database. |
273
|
|
|
* |
274
|
|
|
* @param object $entity |
275
|
|
|
* |
276
|
|
|
* @return array |
277
|
|
|
*/ |
278
|
|
|
public function getOriginalEntityData($entity) |
279
|
|
|
{ |
280
|
|
|
$oid = spl_object_hash($entity); |
281
|
|
|
|
282
|
|
|
if (isset($this->originalEntityData[$oid])) { |
283
|
|
|
return $this->originalEntityData[$oid]; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
return []; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* INTERNAL: |
291
|
|
|
* Checks whether an identifier hash exists in the identity map. |
292
|
|
|
* |
293
|
|
|
* @ignore |
294
|
|
|
* |
295
|
|
|
* @param string $idHash |
296
|
|
|
* @param string $rootClassName |
297
|
|
|
* |
298
|
|
|
* @return boolean |
299
|
|
|
*/ |
300
|
|
|
public function containsIdHash($idHash, $rootClassName) |
301
|
|
|
{ |
302
|
|
|
return isset($this->identityMap[$rootClassName][$idHash]); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* INTERNAL: |
307
|
|
|
* Gets an entity in the identity map by its identifier hash. |
308
|
|
|
* |
309
|
|
|
* @ignore |
310
|
|
|
* |
311
|
|
|
* @param string $idHash |
312
|
|
|
* @param string $rootClassName |
313
|
|
|
* |
314
|
|
|
* @return object |
315
|
|
|
*/ |
316
|
|
|
public function getByIdHash($idHash, $rootClassName) |
317
|
|
|
{ |
318
|
|
|
return $this->identityMap[$rootClassName][$idHash]; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* INTERNAL: |
323
|
|
|
* Tries to get an entity by its identifier hash. If no entity is found for |
324
|
|
|
* the given hash, FALSE is returned. |
325
|
|
|
* |
326
|
|
|
* @ignore |
327
|
|
|
* |
328
|
|
|
* @param mixed $idHash (must be possible to cast it to string) |
329
|
|
|
* @param string $rootClassName |
330
|
|
|
* |
331
|
|
|
* @return object|bool The found entity or FALSE. |
332
|
|
|
*/ |
333
|
|
|
public function tryGetByIdHash($idHash, $rootClassName) |
334
|
|
|
{ |
335
|
|
|
$stringIdHash = (string)$idHash; |
336
|
|
|
|
337
|
|
|
if (isset($this->identityMap[$rootClassName][$stringIdHash])) { |
338
|
|
|
return $this->identityMap[$rootClassName][$stringIdHash]; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
return false; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* Gets the state of an entity with regard to the current unit of work. |
346
|
|
|
* |
347
|
|
|
* @param object $entity |
348
|
|
|
* @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED). |
349
|
|
|
* This parameter can be set to improve performance of entity state detection |
350
|
|
|
* by potentially avoiding a database lookup if the distinction between NEW and DETACHED |
351
|
|
|
* is either known or does not matter for the caller of the method. |
352
|
|
|
* |
353
|
|
|
* @return int The entity state. |
354
|
|
|
*/ |
355
|
|
|
public function getEntityState($entity, $assume = null) |
356
|
|
|
{ |
357
|
|
|
$oid = spl_object_hash($entity); |
358
|
|
|
if (isset($this->entityStates[$oid])) { |
359
|
|
|
return $this->entityStates[$oid]; |
360
|
|
|
} |
361
|
|
|
if ($assume !== null) { |
362
|
|
|
return $assume; |
363
|
|
|
} |
364
|
|
|
// State can only be NEW or DETACHED, because MANAGED/REMOVED states are known. |
365
|
|
|
// Note that you can not remember the NEW or DETACHED state in _entityStates since |
366
|
|
|
// the UoW does not hold references to such objects and the object hash can be reused. |
367
|
|
|
// More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it. |
368
|
|
|
$class = $this->manager->getClassMetadata(get_class($entity)); |
369
|
|
|
$id = $class->getIdentifierValues($entity); |
370
|
|
|
if (!$id) { |
|
|
|
|
371
|
|
|
return self::STATE_NEW; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
return self::STATE_DETACHED; |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* Tries to find an entity with the given identifier in the identity map of |
379
|
|
|
* this UnitOfWork. |
380
|
|
|
* |
381
|
|
|
* @param mixed $id The entity identifier to look for. |
382
|
|
|
* @param string $rootClassName The name of the root class of the mapped entity hierarchy. |
383
|
|
|
* |
384
|
|
|
* @return object|bool Returns the entity with the specified identifier if it exists in |
385
|
|
|
* this UnitOfWork, FALSE otherwise. |
386
|
|
|
*/ |
387
|
11 |
|
public function tryGetById($id, $rootClassName) |
388
|
|
|
{ |
389
|
|
|
/** @var EntityMetadata $metadata */ |
390
|
11 |
|
$metadata = $this->manager->getClassMetadata($rootClassName); |
391
|
11 |
|
$idHash = implode(' ', (array)$this->identifierFlattener->flattenIdentifier($metadata, $id)); |
392
|
|
|
|
393
|
11 |
|
if (isset($this->identityMap[$rootClassName][$idHash])) { |
394
|
4 |
|
return $this->identityMap[$rootClassName][$idHash]; |
395
|
|
|
} |
396
|
|
|
|
397
|
11 |
|
return false; |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* Notifies the listener of a property change. |
402
|
|
|
* |
403
|
|
|
* @param object $sender The object on which the property changed. |
404
|
|
|
* @param string $propertyName The name of the property that changed. |
405
|
|
|
* @param mixed $oldValue The old value of the property that changed. |
406
|
|
|
* @param mixed $newValue The new value of the property that changed. |
407
|
|
|
* |
408
|
|
|
* @return void |
409
|
|
|
*/ |
410
|
|
|
public function propertyChanged($sender, $propertyName, $oldValue, $newValue) |
411
|
|
|
{ |
412
|
|
|
} |
413
|
|
|
} |
414
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.