vendor/mongodb/mongodb/src/functions.php line 506

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