UnitOfWork::flush()   F
last analyzed

Complexity

Conditions 41
Paths 14212

Size

Total Lines 147

Duplication

Lines 10
Ratio 6.8 %

Importance

Changes 0
Metric Value
dl 10
loc 147
rs 0
c 0
b 0
f 0
cc 41
nc 14212
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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(),
0 ignored issues
show
Bug introduced by
The property embeddedSerializer does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
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'])) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\Common\Persistence\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
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;
0 ignored issues
show
Bug introduced by
The variable $oid does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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;
0 ignored issues
show
Bug introduced by
The variable $instance does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
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)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\Common\Persistence\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\Common\Persistence\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
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__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\Common\Persistence\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
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);
0 ignored issues
show
Unused Code introduced by
$oid is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\Common\Persistence\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
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)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldValue of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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));
0 ignored issues
show
Unused Code introduced by
$classMetadata is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $limit defined by parameter $limit on line 1270 can also be of type integer; however, Doctrine\CouchDB\CouchDBClient::findDocuments() does only seem to accept null, maybe add an additional type check?

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.

Loading history...
Bug introduced by
It seems like $offset defined by parameter $offset on line 1270 can also be of type integer; however, Doctrine\CouchDB\CouchDBClient::findDocuments() does only seem to accept null, maybe add an additional type check?

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.

Loading history...
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