1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Doctrine\ODM\CouchDB; |
4
|
|
|
|
5
|
|
|
use Doctrine\CouchDB\Attachment; |
6
|
|
|
use Doctrine\ODM\CouchDB\Mapping\ClassMetadata; |
7
|
|
|
use Doctrine\ODM\CouchDB\Types\Type; |
8
|
|
|
use Doctrine\Common\Collections\Collection; |
9
|
|
|
use Doctrine\Common\Collections\ArrayCollection; |
10
|
|
|
use Doctrine\Common\Persistence\Proxy; |
11
|
|
|
use Doctrine\CouchDB\HTTP\HTTPException; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Unit of work class |
15
|
|
|
* |
16
|
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL |
17
|
|
|
* @link www.doctrine-project.com |
18
|
|
|
* @since 1.0 |
19
|
|
|
* @author Benjamin Eberlei <[email protected]> |
20
|
|
|
* @author Lukas Kahwe Smith <[email protected]> |
21
|
|
|
*/ |
22
|
|
|
class UnitOfWork |
23
|
|
|
{ |
24
|
|
|
const STATE_NEW = 1; |
25
|
|
|
const STATE_MANAGED = 2; |
26
|
|
|
const STATE_REMOVED = 3; |
27
|
|
|
const STATE_DETACHED = 4; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var DocumentManager |
31
|
|
|
*/ |
32
|
|
|
private $dm = null; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var array |
36
|
|
|
*/ |
37
|
|
|
private $identityMap = array(); |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var array |
41
|
|
|
*/ |
42
|
|
|
private $documentIdentifiers = array(); |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var array |
46
|
|
|
*/ |
47
|
|
|
private $documentRevisions = array(); |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var array |
51
|
|
|
*/ |
52
|
|
|
private $documentState = array(); |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* CouchDB always returns and updates the whole data of a document. If on update data is "missing" |
56
|
|
|
* this means the data is deleted. This also applies to attachments. This is why we need to ensure |
57
|
|
|
* that data that is not mapped is not lost. This map here saves all the "left-over" data and keeps |
58
|
|
|
* track of it if necessary. |
59
|
|
|
* |
60
|
|
|
* @var array |
61
|
|
|
*/ |
62
|
|
|
private $nonMappedData = array(); |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* There is no need for a differentiation between original and changeset data in CouchDB, since |
66
|
|
|
* updates have to be complete updates of the document (unless you are using an update handler, which |
67
|
|
|
* is not yet a feature of CouchDB ODM). |
68
|
|
|
* |
69
|
|
|
* @var array |
70
|
|
|
*/ |
71
|
|
|
private $originalData = array(); |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* The original data of embedded document handled separetly from simple property mapping data. |
75
|
|
|
* |
76
|
|
|
* @var array |
77
|
|
|
*/ |
78
|
|
|
private $originalEmbeddedData = array(); |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Contrary to the ORM, CouchDB only knows "updates". The question is wheater a revion exists (Real update vs insert). |
82
|
|
|
* |
83
|
|
|
* @var array |
84
|
|
|
*/ |
85
|
|
|
private $scheduledUpdates = array(); |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* @var array |
89
|
|
|
*/ |
90
|
|
|
private $scheduledRemovals = array(); |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @var array |
94
|
|
|
*/ |
95
|
|
|
private $visitedCollections = array(); |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @var array |
99
|
|
|
*/ |
100
|
|
|
private $idGenerators = array(); |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* @var \Doctrine\Common\EventManager |
104
|
|
|
*/ |
105
|
|
|
private $evm; |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @var \Doctrine\ODM\CouchDB\Mapping\MetadataResolver\MetadataResolver |
109
|
|
|
*/ |
110
|
|
|
private $metadataResolver; |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* @var \Doctrine\ODM\CouchDB\Migrations\DocumentMigration |
114
|
|
|
*/ |
115
|
|
|
private $migrations; |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* @param DocumentManager $dm |
119
|
|
|
*/ |
120
|
|
|
public function __construct(DocumentManager $dm) |
121
|
|
|
{ |
122
|
|
|
$this->dm = $dm; |
123
|
|
|
$this->evm = $dm->getEventManager(); |
124
|
|
|
$this->metadataResolver = $dm->getConfiguration()->getMetadataResolverImpl(); |
125
|
|
|
$this->migrations = $dm->getConfiguration()->getMigrations(); |
126
|
|
|
|
127
|
|
|
$this->embeddedSerializer = new Mapping\EmbeddedDocumentSerializer($this->dm->getMetadataFactory(), |
|
|
|
|
128
|
|
|
$this->metadataResolver); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
private function assertValidDocumentType($documentName, $document, $type) |
132
|
|
|
{ |
133
|
|
|
if ($documentName && !($document instanceof $documentName)) { |
134
|
|
|
throw new InvalidDocumentTypeException($type, $documentName); |
135
|
|
|
} |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Create a document given class, data and the doc-id and revision |
140
|
|
|
* |
141
|
|
|
* @param string $documentName |
142
|
|
|
* @param array $data |
143
|
|
|
* @param array $hints |
144
|
|
|
* |
145
|
|
|
* @return object |
146
|
|
|
* @throws \InvalidArgumentException |
147
|
|
|
* @throws InvalidDocumentTypeException |
148
|
|
|
*/ |
149
|
|
|
public function createDocument($documentName, $data, array &$hints = array()) |
150
|
|
|
{ |
151
|
|
|
$data = $this->migrations->migrate($data); |
152
|
|
|
|
153
|
|
|
if (!$this->metadataResolver->canMapDocument($data)) { |
154
|
|
|
throw new \InvalidArgumentException("Missing or mismatching metadata description in the Document, cannot hydrate!"); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
$type = $this->metadataResolver->getDocumentType($data); |
158
|
|
|
$class = $this->dm->getClassMetadata($type); |
159
|
|
|
|
160
|
|
|
$documentState = array(); |
161
|
|
|
$nonMappedData = array(); |
162
|
|
|
$embeddedDocumentState = array(); |
163
|
|
|
|
164
|
|
|
$id = $data['_id']; |
165
|
|
|
$rev = $data['_rev']; |
166
|
|
|
$conflict = false; |
167
|
|
|
foreach ($data as $jsonName => $jsonValue) { |
168
|
|
|
if (isset($class->jsonNames[$jsonName])) { |
169
|
|
|
$fieldName = $class->jsonNames[$jsonName]; |
170
|
|
|
if (isset($class->fieldMappings[$fieldName])) { |
171
|
|
|
if ($jsonValue === null) { |
172
|
|
|
$documentState[$class->fieldMappings[$fieldName]['fieldName']] = null; |
173
|
|
|
} else if (isset($class->fieldMappings[$fieldName]['embedded'])) { |
174
|
|
|
|
175
|
|
|
$embeddedInstance = |
176
|
|
|
$this->embeddedSerializer->createEmbeddedDocument($jsonValue, $class->fieldMappings[$fieldName]); |
177
|
|
|
|
178
|
|
|
$documentState[$jsonName] = $embeddedInstance; |
179
|
|
|
// storing the jsonValue for embedded docs for now |
180
|
|
|
$embeddedDocumentState[$jsonName] = $jsonValue; |
181
|
|
|
} else { |
182
|
|
|
$documentState[$class->fieldMappings[$fieldName]['fieldName']] = |
183
|
|
|
Type::getType($class->fieldMappings[$fieldName]['type']) |
184
|
|
|
->convertToPHPValue($jsonValue); |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
} else if ($jsonName == '_rev' || $jsonName == "type") { |
188
|
|
|
continue; |
189
|
|
|
} else if ($jsonName == '_conflicts') { |
190
|
|
|
$conflict = true; |
191
|
|
|
} else if ($class->hasAttachments && $jsonName == '_attachments') { |
192
|
|
|
$documentState[$class->attachmentField] = $this->createDocumentAttachments($id, $jsonValue); |
193
|
|
|
} else if ($this->metadataResolver->canResolveJsonField($jsonName)) { |
194
|
|
|
$documentState = $this->metadataResolver->resolveJsonField($class, $this->dm, $documentState, $jsonName, $data); |
195
|
|
|
} else { |
196
|
|
|
$nonMappedData[$jsonName] = $jsonValue; |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
if ($conflict && $this->evm->hasListeners(Event::onConflict)) { |
201
|
|
|
// there is a conflict and we have an event handler that might resolve it |
202
|
|
|
$this->evm->dispatchEvent(Event::onConflict, new Event\ConflictEventArgs($data, $this->dm, $type)); |
203
|
|
|
// the event might be resolved in the couch now, load it again: |
204
|
|
|
return $this->dm->find($type, $id); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
// initialize inverse side collections |
208
|
|
|
foreach ($class->associationsMappings AS $assocName => $assocOptions) { |
209
|
|
|
if (!$assocOptions['isOwning'] && $assocOptions['type'] & ClassMetadata::TO_MANY) { |
210
|
|
|
$documentState[$class->associationsMappings[$assocName]['fieldName']] = new PersistentViewCollection( |
211
|
|
|
new \Doctrine\Common\Collections\ArrayCollection(), |
212
|
|
|
$this->dm, |
213
|
|
|
$id, |
214
|
|
|
$class->associationsMappings[$assocName] |
215
|
|
|
); |
216
|
|
|
} |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
if (isset($this->identityMap[$id])) { |
220
|
|
|
$document = $this->identityMap[$id]; |
221
|
|
|
$overrideLocalValues = false; |
222
|
|
|
|
223
|
|
|
$this->assertValidDocumentType($documentName, $document, $type); |
224
|
|
|
|
225
|
|
|
if ( ($document instanceof Proxy && !$document->__isInitialized__) || isset($hints['refresh'])) { |
|
|
|
|
226
|
|
|
$overrideLocalValues = true; |
227
|
|
|
$oid = spl_object_hash($document); |
228
|
|
|
$this->documentRevisions[$oid] = $rev; |
229
|
|
|
} |
230
|
|
|
} else { |
231
|
|
|
$document = $class->newInstance(); |
232
|
|
|
|
233
|
|
|
$this->assertValidDocumentType($documentName, $document, $type); |
234
|
|
|
|
235
|
|
|
$this->identityMap[$id] = $document; |
236
|
|
|
|
237
|
|
|
$oid = spl_object_hash($document); |
238
|
|
|
$this->documentState[$oid] = self::STATE_MANAGED; |
239
|
|
|
$this->documentIdentifiers[$oid] = (string)$id; |
240
|
|
|
$this->documentRevisions[$oid] = $rev; |
241
|
|
|
$overrideLocalValues = true; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
if ($overrideLocalValues) { |
245
|
|
|
$this->nonMappedData[$oid] = $nonMappedData; |
|
|
|
|
246
|
|
|
foreach ($class->reflFields as $prop => $reflFields) { |
247
|
|
|
$value = isset($documentState[$prop]) ? $documentState[$prop] : null; |
248
|
|
|
if (isset($embeddedDocumentState[$prop])) { |
249
|
|
|
$this->originalEmbeddedData[$oid][$prop] = $embeddedDocumentState[$prop]; |
250
|
|
|
} else { |
251
|
|
|
$this->originalData[$oid][$prop] = $value; |
252
|
|
|
} |
253
|
|
|
$reflFields->setValue($document, $value); |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
257
|
|
View Code Duplication |
if ($this->evm->hasListeners(Event::postLoad)) { |
|
|
|
|
258
|
|
|
$this->evm->dispatchEvent(Event::postLoad, new Event\LifecycleEventArgs($document, $this->dm)); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
return $document; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* @param string $documentId |
266
|
|
|
* @param array $data |
267
|
|
|
* @return array |
268
|
|
|
*/ |
269
|
|
|
private function createDocumentAttachments($documentId, $data) |
270
|
|
|
{ |
271
|
|
|
$attachments = array(); |
272
|
|
|
|
273
|
|
|
$client = $this->dm->getHttpClient(); |
274
|
|
|
$basePath = '/' . $this->dm->getCouchDBClient()->getDatabase() . '/' . $documentId . '/'; |
275
|
|
|
foreach ($data AS $filename => $attachment) { |
276
|
|
|
if (isset($attachment['stub']) && $attachment['stub']) { |
277
|
|
|
$instance = Attachment::createStub($attachment['content_type'], $attachment['length'], $attachment['revpos'], $client, $basePath . $filename); |
278
|
|
|
} else if (isset($attachment['data'])) { |
279
|
|
|
$instance = Attachment::createFromBase64Data($attachment['data'], $attachment['content_type'], $attachment['revpos']); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
$attachments[$filename] = $instance; |
|
|
|
|
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
return $attachments; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
/** |
289
|
|
|
* @param object $document |
290
|
|
|
* @return array |
291
|
|
|
*/ |
292
|
|
|
public function getOriginalData($document) |
293
|
|
|
{ |
294
|
|
|
return $this->originalData[\spl_object_hash($document)]; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Schedule insertion of this document and cascade if neccessary. |
299
|
|
|
* |
300
|
|
|
* @param object $document |
301
|
|
|
*/ |
302
|
|
|
public function scheduleInsert($document) |
303
|
|
|
{ |
304
|
|
|
$visited = array(); |
305
|
|
|
$this->doScheduleInsert($document, $visited); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
private function doScheduleInsert($document, &$visited) |
309
|
|
|
{ |
310
|
|
|
$oid = \spl_object_hash($document); |
311
|
|
|
if (isset($visited[$oid])) { |
312
|
|
|
return; |
313
|
|
|
} |
314
|
|
|
$visited[$oid] = true; |
315
|
|
|
|
316
|
|
|
$class = $this->dm->getClassMetadata(get_class($document)); |
317
|
|
|
$state = $this->getDocumentState($document); |
318
|
|
|
|
319
|
|
|
switch ($state) { |
320
|
|
|
case self::STATE_NEW: |
321
|
|
|
$this->persistNew($class, $document); |
322
|
|
|
break; |
323
|
|
|
case self::STATE_MANAGED: |
324
|
|
|
// TODO: Change Tracking Deferred Explicit |
325
|
|
|
break; |
326
|
|
|
case self::STATE_REMOVED: |
327
|
|
|
// document becomes managed again |
328
|
|
|
unset($this->scheduledRemovals[$oid]); |
329
|
|
|
$this->documentState[$oid] = self::STATE_MANAGED; |
330
|
|
|
break; |
331
|
|
|
case self::STATE_DETACHED: |
332
|
|
|
throw new \InvalidArgumentException("Detached document passed to persist()."); |
333
|
|
|
break; |
|
|
|
|
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
$this->cascadeScheduleInsert($class, $document, $visited); |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* |
341
|
|
|
* @param ClassMetadata $class |
342
|
|
|
* @param object $document |
343
|
|
|
* @param array $visited |
344
|
|
|
*/ |
345
|
|
|
private function cascadeScheduleInsert($class, $document, &$visited) |
346
|
|
|
{ |
347
|
|
|
foreach ($class->associationsMappings AS $assocName => $assoc) { |
348
|
|
|
if ( ($assoc['cascade'] & ClassMetadata::CASCADE_PERSIST) ) { |
349
|
|
|
$related = $class->reflFields[$assocName]->getValue($document); |
350
|
|
|
if (!$related) { |
351
|
|
|
continue; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
if ($class->associationsMappings[$assocName]['type'] & ClassMetadata::TO_ONE) { |
355
|
|
|
if ($this->getDocumentState($related) == self::STATE_NEW) { |
356
|
|
|
$this->doScheduleInsert($related, $visited); |
357
|
|
|
} |
358
|
|
|
} else { |
359
|
|
|
// $related can never be a persistent collection in case of a new entity. |
360
|
|
|
foreach ($related AS $relatedDocument) { |
361
|
|
|
if ($this->getDocumentState($relatedDocument) == self::STATE_NEW) { |
362
|
|
|
$this->doScheduleInsert($relatedDocument, $visited); |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
} |
366
|
|
|
} |
367
|
|
|
} |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
private function getIdGenerator($type) |
371
|
|
|
{ |
372
|
|
|
if (!isset($this->idGenerators[$type])) { |
373
|
|
|
$this->idGenerators[$type] = Id\IdGenerator::create($type); |
374
|
|
|
} |
375
|
|
|
return $this->idGenerators[$type]; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
public function scheduleRemove($document) |
379
|
|
|
{ |
380
|
|
|
$visited = array(); |
381
|
|
|
$this->doRemove($document, $visited); |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
private function doRemove($document, &$visited) |
385
|
|
|
{ |
386
|
|
|
$oid = \spl_object_hash($document); |
387
|
|
|
if (isset($visited[$oid])) { |
388
|
|
|
return; |
389
|
|
|
} |
390
|
|
|
$visited[$oid] = true; |
391
|
|
|
|
392
|
|
|
$this->scheduledRemovals[$oid] = $document; |
393
|
|
|
$this->documentState[$oid] = self::STATE_REMOVED; |
394
|
|
|
|
395
|
|
View Code Duplication |
if ($this->evm->hasListeners(Event::preRemove)) { |
|
|
|
|
396
|
|
|
$this->evm->dispatchEvent(Event::preRemove, new Event\LifecycleEventArgs($document, $this->dm)); |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
$this->cascadeRemove($document, $visited); |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
private function cascadeRemove($document, &$visited) |
403
|
|
|
{ |
404
|
|
|
$class = $this->dm->getClassMetadata(get_class($document)); |
405
|
|
|
foreach ($class->associationsMappings AS $name => $assoc) { |
406
|
|
|
if ($assoc['cascade'] & ClassMetadata::CASCADE_REMOVE) { |
407
|
|
|
$related = $class->reflFields[$assoc['fieldName']]->getValue($document); |
408
|
|
|
if ($related instanceof Collection || is_array($related)) { |
409
|
|
|
// If its a PersistentCollection initialization is intended! No unwrap! |
410
|
|
|
foreach ($related as $relatedDocument) { |
411
|
|
|
$this->doRemove($relatedDocument, $visited); |
412
|
|
|
} |
413
|
|
|
} else if ($related !== null) { |
414
|
|
|
$this->doRemove($related, $visited); |
415
|
|
|
} |
416
|
|
|
} |
417
|
|
|
} |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
public function refresh($document) |
421
|
|
|
{ |
422
|
|
|
$visited = array(); |
423
|
|
|
$this->doRefresh($document, $visited); |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
private function doRefresh($document, &$visited) |
427
|
|
|
{ |
428
|
|
|
$oid = \spl_object_hash($document); |
429
|
|
|
if (isset($visited[$oid])) { |
430
|
|
|
return; |
431
|
|
|
} |
432
|
|
|
$visited[$oid] = true; |
433
|
|
|
|
434
|
|
|
$response = $this->dm->getCouchDBClient()->findDocument($this->getDocumentIdentifier($document)); |
435
|
|
|
|
436
|
|
|
if ($response->status == 404) { |
437
|
|
|
$this->removeFromIdentityMap($document); |
438
|
|
|
throw new \Doctrine\ODM\CouchDB\DocumentNotFoundException(); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
$hints = array('refresh' => true); |
442
|
|
|
$this->createDocument($this->dm->getClassMetadata(get_class($document))->name, $response->body, $hints); |
443
|
|
|
|
444
|
|
|
$this->cascadeRefresh($document, $visited); |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
public function merge($document) |
448
|
|
|
{ |
449
|
|
|
$visited = array(); |
450
|
|
|
return $this->doMerge($document, $visited); |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null) |
454
|
|
|
{ |
455
|
|
|
if (!is_object($document)) { |
456
|
|
|
throw CouchDBException::unexpectedDocumentType($document); |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
$oid = spl_object_hash($document); |
460
|
|
|
if (isset($visited[$oid])) { |
461
|
|
|
return; // Prevent infinite recursion |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
$visited[$oid] = $document; // mark visited |
465
|
|
|
|
466
|
|
|
$class = $this->dm->getClassMetadata(get_class($document)); |
467
|
|
|
|
468
|
|
|
// First we assume DETACHED, although it can still be NEW but we can avoid |
469
|
|
|
// an extra db-roundtrip this way. If it is not MANAGED but has an identity, |
470
|
|
|
// we need to fetch it from the db anyway in order to merge. |
471
|
|
|
// MANAGED entities are ignored by the merge operation. |
472
|
|
|
if ($this->getDocumentState($document) == self::STATE_MANAGED) { |
473
|
|
|
$managedCopy = $document; |
474
|
|
|
} else { |
475
|
|
|
$id = $class->getIdentifierValue($document); |
476
|
|
|
|
477
|
|
|
if (!$id) { |
478
|
|
|
// document is new |
479
|
|
|
// TODO: prePersist will be fired on the empty object?! |
480
|
|
|
$managedCopy = $class->newInstance(); |
481
|
|
|
$this->persistNew($class, $managedCopy); |
482
|
|
|
} else { |
483
|
|
|
$managedCopy = $this->tryGetById($id); |
484
|
|
|
if ($managedCopy) { |
485
|
|
|
// We have the document in-memory already, just make sure its not removed. |
486
|
|
|
if ($this->getDocumentState($managedCopy) == self::STATE_REMOVED) { |
487
|
|
|
throw new \InvalidArgumentException('Removed document detected during merge.' |
488
|
|
|
. ' Can not merge with a removed document.'); |
489
|
|
|
} |
490
|
|
|
} else { |
491
|
|
|
// We need to fetch the managed copy in order to merge. |
492
|
|
|
$managedCopy = $this->dm->find($class->name, $id); |
493
|
|
|
} |
494
|
|
|
|
495
|
|
|
if ($managedCopy === null) { |
496
|
|
|
// If the identifier is ASSIGNED, it is NEW, otherwise an error |
497
|
|
|
// since the managed document was not found. |
498
|
|
|
if ($class->idGenerator == ClassMetadata::IDGENERATOR_ASSIGNED) { |
499
|
|
|
$managedCopy = $class->newInstance(); |
500
|
|
|
$class->setIdentifierValue($managedCopy, $id); |
501
|
|
|
$this->persistNew($class, $managedCopy); |
502
|
|
|
} else { |
503
|
|
|
throw new DocumentNotFoundException(); |
504
|
|
|
} |
505
|
|
|
} |
506
|
|
|
} |
507
|
|
|
|
508
|
|
|
if ($class->isVersioned) { |
509
|
|
|
$managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy); |
510
|
|
|
$documentVersion = $class->reflFields[$class->versionField]->getValue($document); |
511
|
|
|
// Throw exception if versions dont match. |
512
|
|
|
if ($managedCopyVersion != $documentVersion) { |
513
|
|
|
throw OptimisticLockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion); |
514
|
|
|
} |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
$managedOid = spl_object_hash($managedCopy); |
518
|
|
|
// Merge state of $entity into existing (managed) entity |
519
|
|
|
foreach ($class->reflFields as $name => $prop) { |
520
|
|
|
if ( ! isset($class->associationsMappings[$name])) { |
521
|
|
|
if ( ! $class->isIdentifier($name)) { |
522
|
|
|
$prop->setValue($managedCopy, $prop->getValue($document)); |
523
|
|
|
} |
524
|
|
|
} else { |
525
|
|
|
$assoc2 = $class->associationsMappings[$name]; |
526
|
|
|
|
527
|
|
|
if ($assoc2['type'] & ClassMetadata::TO_ONE) { |
528
|
|
|
$other = $prop->getValue($document); |
529
|
|
|
if ($other === null) { |
530
|
|
|
$prop->setValue($managedCopy, null); |
531
|
|
|
} else if ($other instanceof Proxy && !$other->__isInitialized__) { |
|
|
|
|
532
|
|
|
// do not merge fields marked lazy that have not been fetched. |
533
|
|
|
continue; |
534
|
|
|
} else if ( $assoc2['cascade'] & ClassMetadata::CASCADE_MERGE == 0) { |
535
|
|
|
if ($this->getDocumentState($other) == self::STATE_MANAGED) { |
536
|
|
|
$prop->setValue($managedCopy, $other); |
537
|
|
|
} else { |
538
|
|
|
$targetClass = $this->dm->getClassMetadata($assoc2['targetDocument']); |
539
|
|
|
$id = $targetClass->getIdentifierValues($other); |
540
|
|
|
$proxy = $this->dm->getProxyFactory()->getProxy($assoc2['targetDocument'], $id); |
541
|
|
|
$prop->setValue($managedCopy, $proxy); |
542
|
|
|
$this->registerManaged($proxy, $id, null); |
543
|
|
|
} |
544
|
|
|
} |
545
|
|
|
} else { |
546
|
|
|
$mergeCol = $prop->getValue($document); |
547
|
|
|
if ($mergeCol instanceof PersistentCollection && !$mergeCol->isInitialized) { |
548
|
|
|
// do not merge fields marked lazy that have not been fetched. |
549
|
|
|
// keep the lazy persistent collection of the managed copy. |
550
|
|
|
continue; |
551
|
|
|
} |
552
|
|
|
|
553
|
|
|
$managedCol = $prop->getValue($managedCopy); |
554
|
|
|
if (!$managedCol) { |
555
|
|
|
if ($assoc2['isOwning']) { |
556
|
|
|
$managedCol = new PersistentIdsCollection( |
557
|
|
|
new ArrayCollection, |
558
|
|
|
$assoc2['targetDocument'], |
559
|
|
|
$this->dm, |
560
|
|
|
array() |
561
|
|
|
); |
562
|
|
|
} else { |
563
|
|
|
$managedCol = new PersistentViewCollection( |
564
|
|
|
new ArrayCollection, |
565
|
|
|
$this->dm, |
566
|
|
|
$this->documentIdentifiers[$managedOid], |
567
|
|
|
$assoc2 |
568
|
|
|
); |
569
|
|
|
} |
570
|
|
|
$prop->setValue($managedCopy, $managedCol); |
571
|
|
|
$this->originalData[$managedOid][$name] = $managedCol; |
572
|
|
|
} |
573
|
|
|
if ($assoc2['cascade'] & ClassMetadata::CASCADE_MERGE > 0) { |
574
|
|
|
$managedCol->initialize(); |
575
|
|
|
if (!$managedCol->isEmpty()) { |
576
|
|
|
// clear managed collection, in casacadeMerge() the collection is filled again. |
577
|
|
|
$managedCol->unwrap()->clear(); |
578
|
|
|
} |
579
|
|
|
} |
580
|
|
|
} |
581
|
|
|
} |
582
|
|
|
} |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
if ($prevManagedCopy !== null) { |
586
|
|
|
$assocField = $assoc['fieldName']; |
587
|
|
|
$prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy)); |
588
|
|
|
if ($assoc['type'] & ClassMetadata::TO_ONE) { |
589
|
|
|
$prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy); |
590
|
|
|
} else { |
591
|
|
|
$prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy); |
592
|
|
|
if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) { |
593
|
|
|
$class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy); |
594
|
|
|
} |
595
|
|
|
} |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
// Mark the managed copy visited as well |
599
|
|
|
$visited[spl_object_hash($managedCopy)] = true; |
600
|
|
|
|
601
|
|
|
$this->cascadeMerge($document, $managedCopy, $visited); |
602
|
|
|
|
603
|
|
|
return $managedCopy; |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
/** |
607
|
|
|
* Cascades a merge operation to associated entities. |
608
|
|
|
* |
609
|
|
|
* @param object $document |
610
|
|
|
* @param object $managedCopy |
611
|
|
|
* @param array $visited |
612
|
|
|
*/ |
613
|
|
|
private function cascadeMerge($document, $managedCopy, array &$visited) |
614
|
|
|
{ |
615
|
|
|
$class = $this->dm->getClassMetadata(get_class($document)); |
616
|
|
|
foreach ($class->associationsMappings as $assoc) { |
617
|
|
|
if ( $assoc['cascade'] & ClassMetadata::CASCADE_MERGE == 0) { |
618
|
|
|
continue; |
619
|
|
|
} |
620
|
|
|
$relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document); |
621
|
|
|
if ($relatedDocuments instanceof Collection) { |
622
|
|
|
if ($relatedDocuments instanceof PersistentCollection) { |
623
|
|
|
// Unwrap so that foreach() does not initialize |
624
|
|
|
$relatedDocuments = $relatedDocuments->unwrap(); |
625
|
|
|
} |
626
|
|
|
foreach ($relatedDocuments as $relatedDocument) { |
627
|
|
|
$this->doMerge($relatedDocument, $visited, $managedCopy, $assoc); |
628
|
|
|
} |
629
|
|
|
} else if ($relatedDocuments !== null) { |
630
|
|
|
$this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc); |
631
|
|
|
} |
632
|
|
|
} |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
|
636
|
|
|
/** |
637
|
|
|
* Detaches a document from the persistence management. It's persistence will |
638
|
|
|
* no longer be managed by Doctrine. |
639
|
|
|
* |
640
|
|
|
* @param object $document The document to detach. |
641
|
|
|
*/ |
642
|
|
|
public function detach($document) |
643
|
|
|
{ |
644
|
|
|
$visited = array(); |
645
|
|
|
$this->doDetach($document, $visited); |
646
|
|
|
} |
647
|
|
|
|
648
|
|
|
/** |
649
|
|
|
* Executes a detach operation on the given entity. |
650
|
|
|
* |
651
|
|
|
* @param object $document |
652
|
|
|
* @param array $visited |
653
|
|
|
*/ |
654
|
|
|
private function doDetach($document, array &$visited) |
655
|
|
|
{ |
656
|
|
|
$oid = spl_object_hash($document); |
657
|
|
|
if (isset($visited[$oid])) { |
658
|
|
|
return; // Prevent infinite recursion |
659
|
|
|
} |
660
|
|
|
|
661
|
|
|
$visited[$oid] = $document; // mark visited |
662
|
|
|
|
663
|
|
|
switch ($this->getDocumentState($document)) { |
664
|
|
|
case self::STATE_MANAGED: |
665
|
|
|
if (isset($this->identityMap[$this->documentIdentifiers[$oid]])) { |
666
|
|
|
$this->removeFromIdentityMap($document); |
667
|
|
|
} |
668
|
|
|
unset($this->scheduledRemovals[$oid], $this->scheduledUpdates[$oid], |
669
|
|
|
$this->originalData[$oid], $this->documentRevisions[$oid], |
670
|
|
|
$this->documentIdentifiers[$oid], $this->documentState[$oid]); |
671
|
|
|
break; |
672
|
|
|
case self::STATE_NEW: |
673
|
|
|
case self::STATE_DETACHED: |
674
|
|
|
return; |
675
|
|
|
} |
676
|
|
|
|
677
|
|
|
$this->cascadeDetach($document, $visited); |
678
|
|
|
} |
679
|
|
|
|
680
|
|
|
/** |
681
|
|
|
* Cascades a detach operation to associated documents. |
682
|
|
|
* |
683
|
|
|
* @param object $document |
684
|
|
|
* @param array $visited |
685
|
|
|
*/ |
686
|
|
View Code Duplication |
private function cascadeDetach($document, array &$visited) |
|
|
|
|
687
|
|
|
{ |
688
|
|
|
$class = $this->dm->getClassMetadata(get_class($document)); |
689
|
|
|
foreach ($class->associationsMappings as $assoc) { |
690
|
|
|
if ( $assoc['cascade'] & ClassMetadata::CASCADE_DETACH == 0) { |
691
|
|
|
continue; |
692
|
|
|
} |
693
|
|
|
$relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document); |
694
|
|
|
if ($relatedDocuments instanceof Collection) { |
695
|
|
|
if ($relatedDocuments instanceof PersistentCollection) { |
696
|
|
|
// Unwrap so that foreach() does not initialize |
697
|
|
|
$relatedDocuments = $relatedDocuments->unwrap(); |
698
|
|
|
} |
699
|
|
|
foreach ($relatedDocuments as $relatedDocument) { |
700
|
|
|
$this->doDetach($relatedDocument, $visited); |
701
|
|
|
} |
702
|
|
|
} else if ($relatedDocuments !== null) { |
703
|
|
|
$this->doDetach($relatedDocuments, $visited); |
704
|
|
|
} |
705
|
|
|
} |
706
|
|
|
} |
707
|
|
|
|
708
|
|
View Code Duplication |
private function cascadeRefresh($document, &$visited) |
|
|
|
|
709
|
|
|
{ |
710
|
|
|
$class = $this->dm->getClassMetadata(get_class($document)); |
711
|
|
|
foreach ($class->associationsMappings as $assoc) { |
712
|
|
|
if ($assoc['cascade'] & ClassMetadata::CASCADE_REFRESH) { |
713
|
|
|
$related = $class->reflFields[$assoc['fieldName']]->getValue($document); |
714
|
|
|
if ($related instanceof Collection) { |
715
|
|
|
if ($related instanceof PersistentCollection) { |
716
|
|
|
// Unwrap so that foreach() does not initialize |
717
|
|
|
$related = $related->unwrap(); |
718
|
|
|
} |
719
|
|
|
foreach ($related as $relatedDocument) { |
720
|
|
|
$this->doRefresh($relatedDocument, $visited); |
721
|
|
|
} |
722
|
|
|
} else if ($related !== null) { |
723
|
|
|
$this->doRefresh($related, $visited); |
724
|
|
|
} |
725
|
|
|
} |
726
|
|
|
} |
727
|
|
|
} |
728
|
|
|
|
729
|
|
|
/** |
730
|
|
|
* Get the state of a document. |
731
|
|
|
* |
732
|
|
|
* @param object $document |
733
|
|
|
* @return int |
734
|
|
|
*/ |
735
|
|
|
public function getDocumentState($document) |
736
|
|
|
{ |
737
|
|
|
$oid = \spl_object_hash($document); |
738
|
|
|
if (!isset($this->documentState[$oid])) { |
739
|
|
|
$class = $this->dm->getClassMetadata(get_class($document)); |
740
|
|
|
$id = $class->getIdentifierValue($document); |
741
|
|
|
if (!$id) { |
742
|
|
|
return self::STATE_NEW; |
743
|
|
|
} else if ($class->idGenerator == ClassMetadata::IDGENERATOR_ASSIGNED) { |
744
|
|
|
if ($class->isVersioned) { |
745
|
|
|
if ($class->getFieldValue($document, $class->versionField)) { |
746
|
|
|
return self::STATE_DETACHED; |
747
|
|
|
} else { |
748
|
|
|
return self::STATE_NEW; |
749
|
|
|
} |
750
|
|
|
} else { |
751
|
|
|
if ($this->tryGetById($id)) { |
752
|
|
|
return self::STATE_DETACHED; |
753
|
|
|
} else { |
754
|
|
|
$response = $this->dm->getCouchDBClient()->findDocument($id); |
755
|
|
|
|
756
|
|
|
if ($response->status == 404) { |
757
|
|
|
return self::STATE_NEW; |
758
|
|
|
} else { |
759
|
|
|
return self::STATE_DETACHED; |
760
|
|
|
} |
761
|
|
|
} |
762
|
|
|
} |
763
|
|
|
} else { |
764
|
|
|
return self::STATE_DETACHED; |
765
|
|
|
} |
766
|
|
|
} |
767
|
|
|
return $this->documentState[$oid]; |
768
|
|
|
} |
769
|
|
|
|
770
|
|
|
private function detectChangedDocuments() |
771
|
|
|
{ |
772
|
|
|
foreach ($this->identityMap AS $id => $document) { |
773
|
|
|
$state = $this->getDocumentState($document); |
774
|
|
|
if ($state == self::STATE_MANAGED) { |
775
|
|
|
$class = $this->dm->getClassMetadata(get_class($document)); |
776
|
|
|
$this->computeChangeSet($class, $document); |
777
|
|
|
} |
778
|
|
|
} |
779
|
|
|
} |
780
|
|
|
|
781
|
|
|
/** |
782
|
|
|
* @param ClassMetadata $class |
783
|
|
|
* @param object $document |
784
|
|
|
* @return void |
785
|
|
|
*/ |
786
|
|
|
public function computeChangeSet(ClassMetadata $class, $document) |
787
|
|
|
{ |
788
|
|
|
if ($document instanceof Proxy && !$document->__isInitialized__) { |
|
|
|
|
789
|
|
|
return; |
790
|
|
|
} |
791
|
|
|
$oid = \spl_object_hash($document); |
792
|
|
|
$actualData = array(); |
793
|
|
|
$embeddedActualData = array(); |
794
|
|
|
// 1. compute the actual values of the current document |
795
|
|
|
foreach ($class->reflFields AS $fieldName => $reflProperty) { |
796
|
|
|
$value = $reflProperty->getValue($document); |
797
|
|
|
if ($class->isCollectionValuedAssociation($fieldName) && $value !== null |
798
|
|
|
&& !($value instanceof PersistentCollection)) { |
799
|
|
|
|
800
|
|
|
if (!$value instanceof Collection) { |
801
|
|
|
$value = new ArrayCollection($value); |
802
|
|
|
} |
803
|
|
|
|
804
|
|
|
if ($class->associationsMappings[$fieldName]['isOwning']) { |
805
|
|
|
$coll = new PersistentIdsCollection( |
806
|
|
|
$value, |
807
|
|
|
$class->associationsMappings[$fieldName]['targetDocument'], |
808
|
|
|
$this->dm, |
809
|
|
|
array() |
810
|
|
|
); |
811
|
|
|
} else { |
812
|
|
|
$coll = new PersistentViewCollection( |
813
|
|
|
$value, |
814
|
|
|
$this->dm, |
815
|
|
|
$this->documentIdentifiers[$oid], |
816
|
|
|
$class->associationsMappings[$fieldName] |
817
|
|
|
); |
818
|
|
|
} |
819
|
|
|
|
820
|
|
|
$class->reflFields[$fieldName]->setValue($document, $coll); |
821
|
|
|
|
822
|
|
|
$actualData[$fieldName] = $coll; |
823
|
|
|
} else { |
824
|
|
|
$actualData[$fieldName] = $value; |
825
|
|
|
if (isset($class->fieldMappings[$fieldName]['embedded']) && $value !== null) { |
826
|
|
|
// serializing embedded value right here, to be able to detect changes for later invocations |
827
|
|
|
$embeddedActualData[$fieldName] = |
828
|
|
|
$this->embeddedSerializer->serializeEmbeddedDocument($value, $class->fieldMappings[$fieldName]); |
829
|
|
|
} |
830
|
|
|
} |
831
|
|
|
// TODO: ORM transforms arrays and collections into persistent collections |
832
|
|
|
} |
833
|
|
|
// unset the revision field if necessary, it is not to be managed by the user in write scenarios. |
834
|
|
|
if ($class->isVersioned) { |
835
|
|
|
unset($actualData[$class->versionField]); |
836
|
|
|
} |
837
|
|
|
|
838
|
|
|
// 2. Compare to the original, or find out that this document is new. |
839
|
|
|
if (!isset($this->originalData[$oid])) { |
840
|
|
|
// document is New and should be inserted |
841
|
|
|
$this->originalData[$oid] = $actualData; |
842
|
|
|
$this->scheduledUpdates[$oid] = $document; |
843
|
|
|
$this->originalEmbeddedData[$oid] = $embeddedActualData; |
844
|
|
|
} else { |
845
|
|
|
// document is "fully" MANAGED: it was already fully persisted before |
846
|
|
|
// and we have a copy of the original data |
847
|
|
|
|
848
|
|
|
$changed = false; |
849
|
|
|
foreach ($actualData AS $fieldName => $fieldValue) { |
850
|
|
|
// Important to not check embeded values here, because those are objects, equality check isn't enough |
851
|
|
|
// |
852
|
|
|
if (isset($class->fieldMappings[$fieldName]) |
853
|
|
|
&& !isset($class->fieldMappings[$fieldName]['embedded']) |
854
|
|
|
&& $this->originalData[$oid][$fieldName] !== $fieldValue) { |
855
|
|
|
$changed = true; |
856
|
|
|
break; |
857
|
|
|
} else if(isset($class->associationsMappings[$fieldName])) { |
858
|
|
|
if (!$class->associationsMappings[$fieldName]['isOwning']) { |
859
|
|
|
continue; |
860
|
|
|
} |
861
|
|
|
|
862
|
|
|
if ( ($class->associationsMappings[$fieldName]['type'] & ClassMetadata::TO_ONE) && $this->originalData[$oid][$fieldName] !== $fieldValue) { |
863
|
|
|
$changed = true; |
864
|
|
|
break; |
865
|
|
|
} else if ( ($class->associationsMappings[$fieldName]['type'] & ClassMetadata::TO_MANY)) { |
866
|
|
|
if ( !($fieldValue instanceof PersistentCollection)) { |
867
|
|
|
// if its not a persistent collection and the original value changed. otherwise it could just be null |
868
|
|
|
$changed = true; |
869
|
|
|
break; |
870
|
|
|
} else if ($fieldValue->changed()) { |
871
|
|
|
$this->visitedCollections[] = $fieldValue; |
872
|
|
|
$changed = true; |
873
|
|
|
break; |
874
|
|
|
} |
875
|
|
|
} |
876
|
|
|
} else if ($class->hasAttachments && $fieldName == $class->attachmentField) { |
877
|
|
|
// array of value objects, can compare that stricly |
878
|
|
|
if ($this->originalData[$oid][$fieldName] !== $fieldValue) { |
879
|
|
|
$changed = true; |
880
|
|
|
break; |
881
|
|
|
} |
882
|
|
|
} |
883
|
|
|
} |
884
|
|
|
|
885
|
|
|
// Check embedded documents here, only if there is no change yet |
886
|
|
|
if (!$changed) { |
887
|
|
|
foreach ($embeddedActualData as $fieldName => $fieldValue) { |
888
|
|
|
if (!isset($this->originalEmbeddedData[$oid][$fieldName])) { |
889
|
|
|
$changed = true; |
890
|
|
|
break; |
891
|
|
|
} |
892
|
|
|
|
893
|
|
|
$changed = $this->embeddedSerializer->isChanged( |
894
|
|
|
$actualData[$fieldName], /* actual value */ |
895
|
|
|
$this->originalEmbeddedData[$oid][$fieldName], /* original state */ |
896
|
|
|
$class->fieldMappings[$fieldName] |
897
|
|
|
); |
898
|
|
|
|
899
|
|
|
if ($changed) { |
900
|
|
|
break; |
901
|
|
|
} |
902
|
|
|
} |
903
|
|
|
} |
904
|
|
|
|
905
|
|
|
if ($changed) { |
906
|
|
|
$this->originalData[$oid] = $actualData; |
907
|
|
|
$this->scheduledUpdates[$oid] = $document; |
908
|
|
|
$this->originalEmbeddedData[$oid] = $embeddedActualData; |
909
|
|
|
} |
910
|
|
|
} |
911
|
|
|
|
912
|
|
|
// 3. check if any cascading needs to happen |
913
|
|
|
foreach ($class->associationsMappings AS $name => $assoc) { |
914
|
|
|
if ($this->originalData[$oid][$name]) { |
915
|
|
|
$this->computeAssociationChanges($assoc, $this->originalData[$oid][$name]); |
916
|
|
|
} |
917
|
|
|
} |
918
|
|
|
} |
919
|
|
|
|
920
|
|
|
/** |
921
|
|
|
* Computes the changes of an association. |
922
|
|
|
* |
923
|
|
|
* @param array $assoc |
924
|
|
|
* @param mixed $value The value of the association. |
925
|
|
|
* @return \InvalidArgumentException |
926
|
|
|
* @throws \InvalidArgumentException |
927
|
|
|
*/ |
928
|
|
|
private function computeAssociationChanges($assoc, $value) |
929
|
|
|
{ |
930
|
|
|
// Look through the entities, and in any of their associations, for transient (new) |
931
|
|
|
// enities, recursively. ("Persistence by reachability") |
932
|
|
|
if ($assoc['type'] & ClassMetadata::TO_ONE) { |
933
|
|
|
if ($value instanceof Proxy && ! $value->__isInitialized__) { |
|
|
|
|
934
|
|
|
return; // Ignore uninitialized proxy objects |
935
|
|
|
} |
936
|
|
|
$value = array($value); |
937
|
|
|
} else if ($value instanceof PersistentCollection) { |
938
|
|
|
// Unwrap. Uninitialized collections will simply be empty. |
939
|
|
|
$value = $value->unwrap(); |
940
|
|
|
} |
941
|
|
|
|
942
|
|
|
foreach ($value as $entry) { |
943
|
|
|
$targetClass = $this->dm->getClassMetadata($assoc['targetDocument'] ?: get_class($entry)); |
944
|
|
|
$state = $this->getDocumentState($entry); |
945
|
|
|
$oid = spl_object_hash($entry); |
|
|
|
|
946
|
|
|
if ($state == self::STATE_NEW) { |
947
|
|
|
if ( !($assoc['cascade'] & ClassMetadata::CASCADE_PERSIST) ) { |
948
|
|
|
throw new \InvalidArgumentException("A new document was found through a relationship that was not" |
949
|
|
|
. " configured to cascade persist operations: " . self::objToStr($entry) . "." |
950
|
|
|
. " Explicitly persist the new document or configure cascading persist operations" |
951
|
|
|
. " on the relationship."); |
952
|
|
|
} |
953
|
|
|
$this->persistNew($targetClass, $entry); |
954
|
|
|
$this->computeChangeSet($targetClass, $entry); |
955
|
|
|
} else if ($state == self::STATE_REMOVED) { |
956
|
|
|
return new \InvalidArgumentException("Removed document detected during flush: " |
957
|
|
|
. self::objToStr($entry).". Remove deleted documents from associations."); |
958
|
|
|
} else if ($state == self::STATE_DETACHED) { |
959
|
|
|
// Can actually not happen right now as we assume STATE_NEW, |
960
|
|
|
// so the exception will be raised from the DBAL layer (constraint violation). |
961
|
|
|
throw new \InvalidArgumentException("A detached document was found through a " |
962
|
|
|
. "relationship during cascading a persist operation."); |
963
|
|
|
} |
964
|
|
|
// MANAGED associated entities are already taken into account |
965
|
|
|
// during changeset calculation anyway, since they are in the identity map. |
966
|
|
|
} |
967
|
|
|
} |
968
|
|
|
|
969
|
|
|
/** |
970
|
|
|
* Persist new document, marking it managed and generating the id. |
971
|
|
|
* |
972
|
|
|
* This method is either called through `DocumentManager#persist()` or during `DocumentManager#flush()`, |
973
|
|
|
* when persistence by reachability is applied. |
974
|
|
|
* |
975
|
|
|
* @param ClassMetadata $class |
976
|
|
|
* @param object $document |
977
|
|
|
* @return void |
978
|
|
|
*/ |
979
|
|
|
public function persistNew($class, $document) |
980
|
|
|
{ |
981
|
|
|
$id = $this->getIdGenerator($class->idGenerator)->generate($document, $class, $this->dm); |
982
|
|
|
|
983
|
|
|
$this->registerManaged($document, $id, null); |
984
|
|
|
|
985
|
|
View Code Duplication |
if ($this->evm->hasListeners(Event::prePersist)) { |
|
|
|
|
986
|
|
|
$this->evm->dispatchEvent(Event::prePersist, new Event\LifecycleEventArgs($document, $this->dm)); |
987
|
|
|
} |
988
|
|
|
} |
989
|
|
|
|
990
|
|
|
/** |
991
|
|
|
* Flush Operation - Write all dirty entries to the CouchDB. |
992
|
|
|
* |
993
|
|
|
* @throws UpdateConflictException |
994
|
|
|
* @throws CouchDBException |
995
|
|
|
* @throws \Doctrine\CouchDB\HTTP\HTTPException |
996
|
|
|
* @throws DocumentNotFoundException |
997
|
|
|
* |
998
|
|
|
* @return void |
999
|
|
|
*/ |
1000
|
|
|
public function flush() |
1001
|
|
|
{ |
1002
|
|
|
$this->detectChangedDocuments(); |
1003
|
|
|
|
1004
|
|
|
if ($this->evm->hasListeners(Event::onFlush)) { |
1005
|
|
|
$this->evm->dispatchEvent(Event::onFlush, new Event\OnFlushEventArgs($this)); |
1006
|
|
|
} |
1007
|
|
|
|
1008
|
|
|
$config = $this->dm->getConfiguration(); |
1009
|
|
|
|
1010
|
|
|
$bulkUpdater = $this->dm->getCouchDBClient()->createBulkUpdater(); |
1011
|
|
|
$bulkUpdater->setAllOrNothing($config->getAllOrNothingFlush()); |
1012
|
|
|
|
1013
|
|
|
foreach ($this->scheduledRemovals AS $oid => $document) { |
1014
|
|
|
if ($document instanceof Proxy && !$document->__isInitialized__) { |
|
|
|
|
1015
|
|
|
$response = $this->dm->getCouchDBClient()->findDocument($this->getDocumentIdentifier($document)); |
1016
|
|
|
|
1017
|
|
|
if ($response->status == 404) { |
1018
|
|
|
$this->removeFromIdentityMap($document); |
1019
|
|
|
throw new \Doctrine\ODM\CouchDB\DocumentNotFoundException(); |
1020
|
|
|
} |
1021
|
|
|
|
1022
|
|
|
$this->documentRevisions[$oid] = $response->body['_rev']; |
1023
|
|
|
} |
1024
|
|
|
|
1025
|
|
|
$bulkUpdater->deleteDocument($this->documentIdentifiers[$oid], $this->documentRevisions[$oid]); |
1026
|
|
|
$this->removeFromIdentityMap($document); |
1027
|
|
|
|
1028
|
|
View Code Duplication |
if ($this->evm->hasListeners(Event::postRemove)) { |
|
|
|
|
1029
|
|
|
$this->evm->dispatchEvent(Event::postRemove, new Event\LifecycleEventArgs($document, $this->dm)); |
1030
|
|
|
} |
1031
|
|
|
} |
1032
|
|
|
|
1033
|
|
|
foreach ($this->scheduledUpdates AS $oid => $document) { |
1034
|
|
|
$class = $this->dm->getClassMetadata(get_class($document)); |
1035
|
|
|
|
1036
|
|
View Code Duplication |
if ($this->evm->hasListeners(Event::preUpdate)) { |
|
|
|
|
1037
|
|
|
$this->evm->dispatchEvent(Event::preUpdate, new Event\LifecycleEventArgs($document, $this->dm)); |
1038
|
|
|
$this->computeChangeSet($class, $document); // TODO: prevent association computations in this case? |
1039
|
|
|
} |
1040
|
|
|
|
1041
|
|
|
$data = $this->metadataResolver->createDefaultDocumentStruct($class); |
1042
|
|
|
|
1043
|
|
|
// Convert field values to json values. |
1044
|
|
|
foreach ($this->originalData[$oid] AS $fieldName => $fieldValue) { |
1045
|
|
|
if (isset($class->fieldMappings[$fieldName])) { |
1046
|
|
|
if ($fieldValue !== null && isset($class->fieldMappings[$fieldName]['embedded'])) { |
1047
|
|
|
// As we store the serialized value in originalEmbeddedData, we can simply copy here. |
1048
|
|
|
$fieldValue = $this->originalEmbeddedData[$oid][$class->fieldMappings[$fieldName]['jsonName']]; |
1049
|
|
|
|
1050
|
|
|
} else if ($fieldValue !== null) { |
1051
|
|
|
$fieldValue = Type::getType($class->fieldMappings[$fieldName]['type']) |
1052
|
|
|
->convertToCouchDBValue($fieldValue); |
1053
|
|
|
} |
1054
|
|
|
|
1055
|
|
|
if (isset($class->fieldMappings[$fieldName]['id'])) { |
1056
|
|
|
$fieldValue = (string)$fieldValue; |
1057
|
|
|
} |
1058
|
|
|
|
1059
|
|
|
$data[$class->fieldMappings[$fieldName]['jsonName']] = $fieldValue; |
1060
|
|
|
|
1061
|
|
|
} else if (isset($class->associationsMappings[$fieldName])) { |
1062
|
|
|
if ($class->associationsMappings[$fieldName]['type'] & ClassMetadata::TO_ONE) { |
1063
|
|
|
if (\is_object($fieldValue)) { |
1064
|
|
|
$fieldValue = $this->getDocumentIdentifier($fieldValue); |
1065
|
|
|
} else { |
1066
|
|
|
$fieldValue = null; |
1067
|
|
|
} |
1068
|
|
|
$data = $this->metadataResolver->storeAssociationField($data, $class, $this->dm, $fieldName, $fieldValue); |
1069
|
|
|
} else if ($class->associationsMappings[$fieldName]['type'] & ClassMetadata::TO_MANY) { |
1070
|
|
|
if ($class->associationsMappings[$fieldName]['isOwning']) { |
1071
|
|
|
// TODO: Optimize when not initialized yet! In ManyToMany case we can keep track of ALL ids |
1072
|
|
|
$ids = array(); |
1073
|
|
|
if (is_array($fieldValue) || $fieldValue instanceof \Doctrine\Common\Collections\Collection) { |
1074
|
|
|
foreach ($fieldValue AS $key => $relatedObject) { |
1075
|
|
|
$ids[$key] = $this->getDocumentIdentifier($relatedObject); |
1076
|
|
|
} |
1077
|
|
|
} |
1078
|
|
|
$data = $this->metadataResolver->storeAssociationField($data, $class, $this->dm, $fieldName, $ids); |
1079
|
|
|
} |
1080
|
|
|
} |
1081
|
|
|
} else if ($class->hasAttachments && $fieldName == $class->attachmentField) { |
1082
|
|
|
if (is_array($fieldValue) && $fieldValue) { |
|
|
|
|
1083
|
|
|
$data['_attachments'] = array(); |
1084
|
|
|
foreach ($fieldValue AS $filename => $attachment) { |
1085
|
|
|
if (!($attachment instanceof \Doctrine\CouchDB\Attachment)) { |
1086
|
|
|
throw CouchDBException::invalidAttachment($class->name, $this->documentIdentifiers[$oid], $filename); |
1087
|
|
|
} |
1088
|
|
|
$data['_attachments'][$filename] = $attachment->toArray(); |
1089
|
|
|
} |
1090
|
|
|
} |
1091
|
|
|
} |
1092
|
|
|
} |
1093
|
|
|
|
1094
|
|
|
// respect the non mapped data, otherwise they will be deleted. |
1095
|
|
|
if (isset($this->nonMappedData[$oid]) && $this->nonMappedData[$oid]) { |
1096
|
|
|
$data = array_merge($data, $this->nonMappedData[$oid]); |
1097
|
|
|
} |
1098
|
|
|
|
1099
|
|
|
$rev = $this->getDocumentRevision($document); |
1100
|
|
|
if ($rev) { |
1101
|
|
|
$data['_rev'] = $rev; |
1102
|
|
|
} |
1103
|
|
|
$data['_id'] = $this->documentIdentifiers[$oid]; |
1104
|
|
|
|
1105
|
|
|
$bulkUpdater->updateDocument($data); |
1106
|
|
|
} |
1107
|
|
|
$response = $bulkUpdater->execute(); |
1108
|
|
|
$updateConflictDocuments = array(); |
1109
|
|
|
if ($response->status == 201) { |
1110
|
|
|
foreach ($response->body AS $docResponse) { |
1111
|
|
|
if (!isset($this->identityMap[$docResponse['id']])) { |
1112
|
|
|
// deletions |
1113
|
|
|
continue; |
1114
|
|
|
} |
1115
|
|
|
|
1116
|
|
|
$document = $this->identityMap[$docResponse['id']]; |
1117
|
|
|
if (isset($docResponse['error'])) { |
1118
|
|
|
$updateConflictDocuments[] = $document; |
1119
|
|
|
} else { |
1120
|
|
|
$this->documentRevisions[spl_object_hash($document)] = $docResponse['rev']; |
1121
|
|
|
$class = $this->dm->getClassMetadata(get_class($document)); |
1122
|
|
|
if ($class->isVersioned) { |
1123
|
|
|
$class->reflFields[$class->versionField]->setValue($document, $docResponse['rev']); |
1124
|
|
|
} |
1125
|
|
|
} |
1126
|
|
|
|
1127
|
|
View Code Duplication |
if ($this->evm->hasListeners(Event::postUpdate)) { |
|
|
|
|
1128
|
|
|
$this->evm->dispatchEvent(Event::postUpdate, new Event\LifecycleEventArgs($document, $this->dm)); |
1129
|
|
|
} |
1130
|
|
|
} |
1131
|
|
|
} else if ($response->status >= 400) { |
1132
|
|
|
throw HTTPException::fromResponse($bulkUpdater->getPath(), $response); |
1133
|
|
|
} |
1134
|
|
|
|
1135
|
|
|
foreach ($this->visitedCollections AS $col) { |
1136
|
|
|
$col->takeSnapshot(); |
1137
|
|
|
} |
1138
|
|
|
|
1139
|
|
|
$this->scheduledUpdates = |
1140
|
|
|
$this->scheduledRemovals = |
1141
|
|
|
$this->visitedCollections = array(); |
1142
|
|
|
|
1143
|
|
|
if (count($updateConflictDocuments)) { |
1144
|
|
|
throw new UpdateConflictException($updateConflictDocuments); |
1145
|
|
|
} |
1146
|
|
|
} |
1147
|
|
|
|
1148
|
|
|
/** |
1149
|
|
|
* INTERNAL: |
1150
|
|
|
* Removes an document from the identity map. This effectively detaches the |
1151
|
|
|
* document from the persistence management of Doctrine. |
1152
|
|
|
* |
1153
|
|
|
* @ignore |
1154
|
|
|
* @param object $document |
1155
|
|
|
* @return boolean |
1156
|
|
|
*/ |
1157
|
|
|
public function removeFromIdentityMap($document) |
1158
|
|
|
{ |
1159
|
|
|
$oid = spl_object_hash($document); |
1160
|
|
|
|
1161
|
|
|
if (isset($this->identityMap[$this->documentIdentifiers[$oid]])) { |
1162
|
|
|
unset($this->identityMap[$this->documentIdentifiers[$oid]], |
1163
|
|
|
$this->documentIdentifiers[$oid], |
1164
|
|
|
$this->documentRevisions[$oid], |
1165
|
|
|
$this->documentState[$oid]); |
1166
|
|
|
|
1167
|
|
|
return true; |
1168
|
|
|
} |
1169
|
|
|
|
1170
|
|
|
return false; |
1171
|
|
|
} |
1172
|
|
|
|
1173
|
|
|
/** |
1174
|
|
|
* @param object $document |
1175
|
|
|
* @return bool |
1176
|
|
|
*/ |
1177
|
|
|
public function contains($document) |
1178
|
|
|
{ |
1179
|
|
|
return isset($this->documentIdentifiers[\spl_object_hash($document)]); |
1180
|
|
|
} |
1181
|
|
|
|
1182
|
|
|
public function registerManaged($document, $identifier, $revision) |
1183
|
|
|
{ |
1184
|
|
|
$oid = spl_object_hash($document); |
1185
|
|
|
$this->documentState[$oid] = self::STATE_MANAGED; |
1186
|
|
|
$this->documentIdentifiers[$oid] = (string)$identifier; |
1187
|
|
|
$this->documentRevisions[$oid] = $revision; |
1188
|
|
|
$this->identityMap[$identifier] = $document; |
1189
|
|
|
} |
1190
|
|
|
|
1191
|
|
|
/** |
1192
|
|
|
* Tries to find an document with the given identifier in the identity map of |
1193
|
|
|
* this UnitOfWork. |
1194
|
|
|
* |
1195
|
|
|
* @param mixed $id The document identifier to look for. |
1196
|
|
|
* @return mixed Returns the document with the specified identifier if it exists in |
1197
|
|
|
* this UnitOfWork, FALSE otherwise. |
1198
|
|
|
*/ |
1199
|
|
|
public function tryGetById($id) |
1200
|
|
|
{ |
1201
|
|
|
if (isset($this->identityMap[$id])) { |
1202
|
|
|
return $this->identityMap[$id]; |
1203
|
|
|
} |
1204
|
|
|
return false; |
1205
|
|
|
} |
1206
|
|
|
|
1207
|
|
|
/** |
1208
|
|
|
* Checks whether a document is registered in the identity map of this UnitOfWork. |
1209
|
|
|
* |
1210
|
|
|
* @param object $document |
1211
|
|
|
* @return boolean |
1212
|
|
|
*/ |
1213
|
|
|
public function isInIdentityMap($document) |
1214
|
|
|
{ |
1215
|
|
|
$oid = spl_object_hash($document); |
1216
|
|
|
if ( ! isset($this->documentIdentifiers[$oid])) { |
1217
|
|
|
return false; |
1218
|
|
|
} |
1219
|
|
|
$classMetadata = $this->dm->getClassMetadata(get_class($document)); |
|
|
|
|
1220
|
|
|
if ($this->documentIdentifiers[$oid] === '') { |
1221
|
|
|
return false; |
1222
|
|
|
} |
1223
|
|
|
|
1224
|
|
|
return isset($this->identityMap[$this->documentIdentifiers[$oid]]); |
1225
|
|
|
} |
1226
|
|
|
|
1227
|
|
|
/** |
1228
|
|
|
* Get the CouchDB revision of the document that was current upon retrieval. |
1229
|
|
|
* |
1230
|
|
|
* @throws CouchDBException |
1231
|
|
|
* @param object $document |
1232
|
|
|
* @return string |
1233
|
|
|
*/ |
1234
|
|
|
public function getDocumentRevision($document) |
1235
|
|
|
{ |
1236
|
|
|
$oid = \spl_object_hash($document); |
1237
|
|
|
if (isset($this->documentRevisions[$oid])) { |
1238
|
|
|
return $this->documentRevisions[$oid]; |
1239
|
|
|
} |
1240
|
|
|
return null; |
1241
|
|
|
} |
1242
|
|
|
|
1243
|
|
|
public function getDocumentIdentifier($document) |
1244
|
|
|
{ |
1245
|
|
|
$oid = \spl_object_hash($document); |
1246
|
|
|
if (isset($this->documentIdentifiers[$oid])) { |
1247
|
|
|
return $this->documentIdentifiers[$oid]; |
1248
|
|
|
} else { |
1249
|
|
|
throw new CouchDBException("Document is not managed and has no identifier."); |
1250
|
|
|
} |
1251
|
|
|
} |
1252
|
|
|
|
1253
|
|
|
private static function objToStr($obj) |
1254
|
|
|
{ |
1255
|
|
|
return method_exists($obj, '__toString') ? (string)$obj : get_class($obj).'@'.spl_object_hash($obj); |
1256
|
|
|
} |
1257
|
|
|
|
1258
|
|
|
/** |
1259
|
|
|
* Find many documents by id. |
1260
|
|
|
* |
1261
|
|
|
* Important: Each document is returned with the key it has in the $ids array! |
1262
|
|
|
* |
1263
|
|
|
* @param array $ids |
1264
|
|
|
* @param null|string $documentName |
1265
|
|
|
* @param null|int $limit |
1266
|
|
|
* @param null|int $offset |
1267
|
|
|
* @return array |
1268
|
|
|
* @throws \Exception |
1269
|
|
|
*/ |
1270
|
|
|
public function findMany(array $ids, $documentName = null, $limit = null, $offset = null) |
1271
|
|
|
{ |
1272
|
|
|
$response = $this->dm->getCouchDBClient()->findDocuments($ids, $limit, $offset); |
|
|
|
|
1273
|
|
|
$keys = array_flip($ids); |
1274
|
|
|
|
1275
|
|
|
if ($response->status != 200) { |
1276
|
|
|
throw new \Exception("loadMany error code " . $response->status); |
1277
|
|
|
} |
1278
|
|
|
|
1279
|
|
|
$docs = array(); |
1280
|
|
|
foreach ($response->body['rows'] AS $responseData) { |
1281
|
|
|
if (isset($responseData['doc'])) { |
1282
|
|
|
$docs[$keys[$responseData['id']]] = $this->createDocument($documentName, $responseData['doc']); |
1283
|
|
|
} |
1284
|
|
|
} |
1285
|
|
|
return $docs; |
1286
|
|
|
} |
1287
|
|
|
|
1288
|
|
|
/** |
1289
|
|
|
* Get all entries currently in the identity map |
1290
|
|
|
* |
1291
|
|
|
* @return array |
1292
|
|
|
*/ |
1293
|
|
|
public function getIdentityMap() |
1294
|
|
|
{ |
1295
|
|
|
return $this->identityMap; |
1296
|
|
|
} |
1297
|
|
|
} |
1298
|
|
|
|
In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:
Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion: