doctrine /
couchdb-odm
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 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
|
|||
| 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
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
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
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
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
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
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
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
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 function fx() {
try {
doSomething();
return true;
}
catch (\Exception $e) {
return false;
}
return false;
}
In the above example, the last 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
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
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
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
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
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
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
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
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
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
$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 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
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
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
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
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
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
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 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
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
$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 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
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...
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 |
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: