See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
1 <?php 2 /* 3 * Copyright 2015-present MongoDB, Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * https://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 namespace MongoDB; 19 20 use Exception; 21 use MongoDB\BSON\Serializable; 22 use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; 23 use MongoDB\Driver\Manager; 24 use MongoDB\Driver\ReadPreference; 25 use MongoDB\Driver\Server; 26 use MongoDB\Driver\Session; 27 use MongoDB\Driver\WriteConcern; 28 use MongoDB\Exception\InvalidArgumentException; 29 use MongoDB\Exception\RuntimeException; 30 use MongoDB\Operation\ListCollections; 31 use MongoDB\Operation\WithTransaction; 32 use ReflectionClass; 33 use ReflectionException; 34 35 use function assert; 36 use function end; 37 use function get_object_vars; 38 use function in_array; 39 use function is_array; 40 use function is_object; 41 use function is_string; 42 use function key; 43 use function MongoDB\BSON\fromPHP; 44 use function MongoDB\BSON\toPHP; 45 use function reset; 46 use function substr; 47 48 /** 49 * Check whether all servers support executing a write stage on a secondary. 50 * 51 * @internal 52 * @param Server[] $servers 53 */ 54 function all_servers_support_write_stage_on_secondary(array $servers): bool 55 { 56 /* Write stages on secondaries are technically supported by FCV 4.4, but the 57 * CRUD spec requires all 5.0+ servers since FCV is not tracked by SDAM. */ 58 static $wireVersionForWriteStageOnSecondary = 13; 59 60 foreach ($servers as $server) { 61 // We can assume that load balancers only front 5.0+ servers 62 if ($server->getType() === Server::TYPE_LOAD_BALANCER) { 63 continue; 64 } 65 66 if (! server_supports_feature($server, $wireVersionForWriteStageOnSecondary)) { 67 return false; 68 } 69 } 70 71 return true; 72 } 73 74 /** 75 * Applies a type map to a document. 76 * 77 * This function is used by operations where it is not possible to apply a type 78 * map to the cursor directly because the root document is a command response 79 * (e.g. findAndModify). 80 * 81 * @internal 82 * @param array|object $document Document to which the type map will be applied 83 * @param array $typeMap Type map for BSON deserialization. 84 * @return array|object 85 * @throws InvalidArgumentException 86 */ 87 function apply_type_map_to_document($document, array $typeMap) 88 { 89 if (! is_array($document) && ! is_object($document)) { 90 throw InvalidArgumentException::invalidType('$document', $document, 'array or object'); 91 } 92 93 return toPHP(fromPHP($document), $typeMap); 94 } 95 96 /** 97 * Generate an index name from a key specification. 98 * 99 * @internal 100 * @param array|object $document Document containing fields mapped to values, 101 * which denote order or an index type 102 * @throws InvalidArgumentException 103 */ 104 function generate_index_name($document): string 105 { 106 if ($document instanceof Serializable) { 107 $document = $document->bsonSerialize(); 108 } 109 110 if (is_object($document)) { 111 $document = get_object_vars($document); 112 } 113 114 if (! is_array($document)) { 115 throw InvalidArgumentException::invalidType('$document', $document, 'array or object'); 116 } 117 118 $name = ''; 119 120 foreach ($document as $field => $type) { 121 $name .= ($name != '' ? '_' : '') . $field . '_' . $type; 122 } 123 124 return $name; 125 } 126 127 /** 128 * Return a collection's encryptedFields from the encryptedFieldsMap 129 * autoEncryption driver option (if available). 130 * 131 * @internal 132 * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#drop-collection-helper 133 * @see Collection::drop 134 * @see Database::createCollection 135 * @see Database::dropCollection 136 * @return array|object|null 137 */ 138 function get_encrypted_fields_from_driver(string $databaseName, string $collectionName, Manager $manager) 139 { 140 $encryptedFieldsMap = (array) $manager->getEncryptedFieldsMap(); 141 142 return $encryptedFieldsMap[$databaseName . '.' . $collectionName] ?? null; 143 } 144 145 /** 146 * Return a collection's encryptedFields option from the server (if any). 147 * 148 * @internal 149 * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#drop-collection-helper 150 * @see Collection::drop 151 * @see Database::dropCollection 152 * @return array|object|null 153 */ 154 function get_encrypted_fields_from_server(string $databaseName, string $collectionName, Manager $manager, Server $server) 155 { 156 // No-op if the encryptedFieldsMap autoEncryption driver option was omitted 157 if ($manager->getEncryptedFieldsMap() === null) { 158 return null; 159 } 160 161 $collectionInfoIterator = (new ListCollections($databaseName, ['filter' => ['name' => $collectionName]]))->execute($server); 162 163 foreach ($collectionInfoIterator as $collectionInfo) { 164 /* Note: ListCollections applies a typeMap that converts BSON documents 165 * to PHP arrays. This should not be problematic as encryptedFields here 166 * is only used by drop helpers to obtain names of supporting encryption 167 * collections. */ 168 return $collectionInfo['options']['encryptedFields'] ?? null; 169 } 170 171 return null; 172 } 173 174 /** 175 * Return whether the first key in the document starts with a "$" character. 176 * 177 * This is used for differentiating update and replacement documents. 178 * 179 * @internal 180 * @param array|object $document Update or replacement document 181 * @throws InvalidArgumentException 182 */ 183 function is_first_key_operator($document): bool 184 { 185 if ($document instanceof Serializable) { 186 $document = $document->bsonSerialize(); 187 } 188 189 if (is_object($document)) { 190 $document = get_object_vars($document); 191 } 192 193 if (! is_array($document)) { 194 throw InvalidArgumentException::invalidType('$document', $document, 'array or object'); 195 } 196 197 reset($document); 198 $firstKey = (string) key($document); 199 200 return isset($firstKey[0]) && $firstKey[0] === '$'; 201 } 202 203 /** 204 * Returns whether an update specification is a valid aggregation pipeline. 205 * 206 * @internal 207 * @param mixed $pipeline 208 */ 209 function is_pipeline($pipeline): bool 210 { 211 if (! is_array($pipeline)) { 212 return false; 213 } 214 215 if ($pipeline === []) { 216 return false; 217 } 218 219 $expectedKey = 0; 220 221 foreach ($pipeline as $key => $stage) { 222 if (! is_array($stage) && ! is_object($stage)) { 223 return false; 224 } 225 226 if ($expectedKey !== $key) { 227 return false; 228 } 229 230 $expectedKey++; 231 $stage = (array) $stage; 232 reset($stage); 233 $key = key($stage); 234 235 if (! is_string($key) || substr($key, 0, 1) !== '$') { 236 return false; 237 } 238 } 239 240 return true; 241 } 242 243 /** 244 * Returns whether we are currently in a transaction. 245 * 246 * @internal 247 * @param array $options Command options 248 */ 249 function is_in_transaction(array $options): bool 250 { 251 if (isset($options['session']) && $options['session'] instanceof Session && $options['session']->isInTransaction()) { 252 return true; 253 } 254 255 return false; 256 } 257 258 /** 259 * Return whether the aggregation pipeline ends with an $out or $merge operator. 260 * 261 * This is used for determining whether the aggregation pipeline must be 262 * executed against a primary server. 263 * 264 * @internal 265 * @param array $pipeline List of pipeline operations 266 */ 267 function is_last_pipeline_operator_write(array $pipeline): bool 268 { 269 $lastOp = end($pipeline); 270 271 if ($lastOp === false) { 272 return false; 273 } 274 275 $lastOp = (array) $lastOp; 276 277 return in_array(key($lastOp), ['$out', '$merge'], true); 278 } 279 280 /** 281 * Return whether the "out" option for a mapReduce operation is "inline". 282 * 283 * This is used to determine if a mapReduce command requires a primary. 284 * 285 * @internal 286 * @see https://mongodb.com/docs/manual/reference/command/mapReduce/#output-inline 287 * @param string|array|object $out Output specification 288 * @throws InvalidArgumentException 289 */ 290 function is_mapreduce_output_inline($out): bool 291 { 292 if (! is_array($out) && ! is_object($out)) { 293 return false; 294 } 295 296 if ($out instanceof Serializable) { 297 $out = $out->bsonSerialize(); 298 } 299 300 if (is_object($out)) { 301 $out = get_object_vars($out); 302 } 303 304 if (! is_array($out)) { 305 throw InvalidArgumentException::invalidType('$out', $out, 'array or object'); 306 } 307 308 reset($out); 309 310 return key($out) === 'inline'; 311 } 312 313 /** 314 * Return whether the write concern is acknowledged. 315 * 316 * This function is similar to mongoc_write_concern_is_acknowledged but does not 317 * check the fsync option since that was never supported in the PHP driver. 318 * 319 * @internal 320 * @see https://mongodb.com/docs/manual/reference/write-concern/ 321 */ 322 function is_write_concern_acknowledged(WriteConcern $writeConcern): bool 323 { 324 /* Note: -1 corresponds to MONGOC_WRITE_CONCERN_W_ERRORS_IGNORED, which is 325 * deprecated synonym of MONGOC_WRITE_CONCERN_W_UNACKNOWLEDGED and slated 326 * for removal in libmongoc 2.0. */ 327 return ($writeConcern->getW() !== 0 && $writeConcern->getW() !== -1) || $writeConcern->getJournal() === true; 328 } 329 330 /** 331 * Return whether the server supports a particular feature. 332 * 333 * @internal 334 * @param Server $server Server to check 335 * @param integer $feature Feature constant (i.e. wire protocol version) 336 */ 337 function server_supports_feature(Server $server, int $feature): bool 338 { 339 $info = $server->getInfo(); 340 $maxWireVersion = isset($info['maxWireVersion']) ? (integer) $info['maxWireVersion'] : 0; 341 $minWireVersion = isset($info['minWireVersion']) ? (integer) $info['minWireVersion'] : 0; 342 343 return $minWireVersion <= $feature && $maxWireVersion >= $feature; 344 } 345 346 /** 347 * Return whether the input is an array of strings. 348 * 349 * @internal 350 * @param mixed $input 351 */ 352 function is_string_array($input): bool 353 { 354 if (! is_array($input)) { 355 return false; 356 } 357 358 foreach ($input as $item) { 359 if (! is_string($item)) { 360 return false; 361 } 362 } 363 364 return true; 365 } 366 367 /** 368 * Performs a deep copy of a value. 369 * 370 * This function will clone objects and recursively copy values within arrays. 371 * 372 * @internal 373 * @see https://bugs.php.net/bug.php?id=49664 374 * @param mixed $element Value to be copied 375 * @return mixed 376 * @throws ReflectionException 377 */ 378 function recursive_copy($element) 379 { 380 if (is_array($element)) { 381 foreach ($element as $key => $value) { 382 $element[$key] = recursive_copy($value); 383 } 384 385 return $element; 386 } 387 388 if (! is_object($element)) { 389 return $element; 390 } 391 392 if (! (new ReflectionClass($element))->isCloneable()) { 393 return $element; 394 } 395 396 return clone $element; 397 } 398 399 /** 400 * Creates a type map to apply to a field type 401 * 402 * This is used in the Aggregate, Distinct, and FindAndModify operations to 403 * apply the root-level type map to the document that will be returned. It also 404 * replaces the root type with object for consistency within these operations 405 * 406 * An existing type map for the given field path will not be overwritten 407 * 408 * @internal 409 * @param array $typeMap The existing typeMap 410 * @param string $fieldPath The field path to apply the root type to 411 */ 412 function create_field_path_type_map(array $typeMap, string $fieldPath): array 413 { 414 // If some field paths already exist, we prefix them with the field path we are assuming as the new root 415 if (isset($typeMap['fieldPaths']) && is_array($typeMap['fieldPaths'])) { 416 $fieldPaths = $typeMap['fieldPaths']; 417 418 $typeMap['fieldPaths'] = []; 419 foreach ($fieldPaths as $existingFieldPath => $type) { 420 $typeMap['fieldPaths'][$fieldPath . '.' . $existingFieldPath] = $type; 421 } 422 } 423 424 // If a root typemap was set, apply this to the field object 425 if (isset($typeMap['root'])) { 426 $typeMap['fieldPaths'][$fieldPath] = $typeMap['root']; 427 } 428 429 /* Special case if we want to convert an array, in which case we need to 430 * ensure that the field containing the array is exposed as an array, 431 * instead of the type given in the type map's array key. */ 432 if (substr($fieldPath, -2, 2) === '.$') { 433 $typeMap['fieldPaths'][substr($fieldPath, 0, -2)] = 'array'; 434 } 435 436 $typeMap['root'] = 'object'; 437 438 return $typeMap; 439 } 440 441 /** 442 * Execute a callback within a transaction in the given session 443 * 444 * This helper takes care of retrying the commit operation or the entire 445 * transaction if an error occurs. 446 * 447 * If the commit fails because of an UnknownTransactionCommitResult error, the 448 * commit is retried without re-invoking the callback. 449 * If the commit fails because of a TransientTransactionError, the entire 450 * transaction will be retried. In this case, the callback will be invoked 451 * again. It is important that the logic inside the callback is idempotent. 452 * 453 * In case of failures, the commit or transaction are retried until 120 seconds 454 * from the initial call have elapsed. After that, no retries will happen and 455 * the helper will throw the last exception received from the driver. 456 * 457 * @see Client::startSession 458 * @see Session::startTransaction for supported transaction options 459 * 460 * @param Session $session A session object as retrieved by Client::startSession 461 * @param callable $callback A callback that will be invoked within the transaction 462 * @param array $transactionOptions Additional options that are passed to Session::startTransaction 463 * @throws RuntimeException for driver errors while committing the transaction 464 * @throws Exception for any other errors, including those thrown in the callback 465 */ 466 function with_transaction(Session $session, callable $callback, array $transactionOptions = []): void 467 { 468 $operation = new WithTransaction($callback, $transactionOptions); 469 $operation->execute($session); 470 } 471 472 /** 473 * Returns the session option if it is set and valid. 474 * 475 * @internal 476 */ 477 function extract_session_from_options(array $options): ?Session 478 { 479 if (! isset($options['session']) || ! $options['session'] instanceof Session) { 480 return null; 481 } 482 483 return $options['session']; 484 } 485 486 /** 487 * Returns the readPreference option if it is set and valid. 488 * 489 * @internal 490 */ 491 function extract_read_preference_from_options(array $options): ?ReadPreference 492 { 493 if (! isset($options['readPreference']) || ! $options['readPreference'] instanceof ReadPreference) { 494 return null; 495 } 496 497 return $options['readPreference']; 498 } 499 500 /** 501 * Performs server selection, respecting the readPreference and session options 502 * (if given) 503 * 504 * @internal 505 */ 506 function select_server(Manager $manager, array $options): Server 507 { 508 $session = extract_session_from_options($options); 509 $server = $session instanceof Session ? $session->getServer() : null; 510 if ($server !== null) { 511 return $server; 512 } 513 514 $readPreference = extract_read_preference_from_options($options); 515 if (! $readPreference instanceof ReadPreference) { 516 // TODO: PHPLIB-476: Read transaction read preference once PHPC-1439 is implemented 517 $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); 518 } 519 520 return $manager->selectServer($readPreference); 521 } 522 523 /** 524 * Performs server selection for an aggregate operation with a write stage. The 525 * $options parameter may be modified by reference if a primary read preference 526 * must be forced due to the existence of pre-5.0 servers in the topology. 527 * 528 * @internal 529 * @see https://github.com/mongodb/specifications/blob/master/source/crud/crud.rst#aggregation-pipelines-with-write-stages 530 */ 531 function select_server_for_aggregate_write_stage(Manager $manager, array &$options): Server 532 { 533 $readPreference = extract_read_preference_from_options($options); 534 535 /* If there is either no read preference or a primary read preference, there 536 * is no special server selection logic to apply. */ 537 if ($readPreference === null || $readPreference->getMode() === ReadPreference::RP_PRIMARY) { 538 return select_server($manager, $options); 539 } 540 541 $server = null; 542 $serverSelectionError = null; 543 544 try { 545 $server = select_server($manager, $options); 546 } catch (DriverRuntimeException $serverSelectionError) { 547 } 548 549 /* If any pre-5.0 servers exist in the topology, force a primary read 550 * preference and repeat server selection if it previously failed or 551 * selected a secondary. */ 552 if (! all_servers_support_write_stage_on_secondary($manager->getServers())) { 553 $options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY); 554 555 if ($server === null || $server->isSecondary()) { 556 return select_server($manager, $options); 557 } 558 } 559 560 /* If the topology only contains 5.0+ servers, we should either return the 561 * previously selected server or propagate the server selection error. */ 562 if ($serverSelectionError !== null) { 563 throw $serverSelectionError; 564 } 565 566 assert($server instanceof Server); 567 568 return $server; 569 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body