Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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\Operation;
  19  
  20  use MongoDB\Driver\Command;
  21  use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
  22  use MongoDB\Driver\Server;
  23  use MongoDB\Driver\Session;
  24  use MongoDB\Driver\WriteConcern;
  25  use MongoDB\Exception\InvalidArgumentException;
  26  use MongoDB\Exception\UnexpectedValueException;
  27  use MongoDB\Exception\UnsupportedException;
  28  
  29  use function array_key_exists;
  30  use function current;
  31  use function is_array;
  32  use function is_bool;
  33  use function is_integer;
  34  use function is_object;
  35  use function is_string;
  36  use function MongoDB\create_field_path_type_map;
  37  use function MongoDB\is_pipeline;
  38  use function MongoDB\is_write_concern_acknowledged;
  39  use function MongoDB\server_supports_feature;
  40  
  41  /**
  42   * Operation for the findAndModify command.
  43   *
  44   * This class is used internally by the FindOneAndDelete, FindOneAndReplace, and
  45   * FindOneAndUpdate operation classes.
  46   *
  47   * @internal
  48   * @see https://mongodb.com/docs/manual/reference/command/findAndModify/
  49   */
  50  class FindAndModify implements Executable, Explainable
  51  {
  52      /** @var integer */
  53      private static $wireVersionForHint = 9;
  54  
  55      /** @var integer */
  56      private static $wireVersionForUnsupportedOptionServerSideError = 8;
  57  
  58      /** @var string */
  59      private $databaseName;
  60  
  61      /** @var string */
  62      private $collectionName;
  63  
  64      /** @var array */
  65      private $options;
  66  
  67      /**
  68       * Constructs a findAndModify command.
  69       *
  70       * Supported options:
  71       *
  72       *  * arrayFilters (document array): A set of filters specifying to which
  73       *    array elements an update should apply.
  74       *
  75       *  * collation (document): Collation specification.
  76       *
  77       *  * comment (mixed): BSON value to attach as a comment to this command.
  78       *
  79       *    This is not supported for servers versions < 4.4.
  80       *
  81       *  * bypassDocumentValidation (boolean): If true, allows the write to
  82       *    circumvent document level validation.
  83       *
  84       *  * fields (document): Limits the fields to return for the matching
  85       *    document.
  86       *
  87       *  * hint (string|document): The index to use. Specify either the index
  88       *    name as a string or the index key pattern as a document. If specified,
  89       *    then the query system will only consider plans using the hinted index.
  90       *
  91       *    This is only supported on server versions >= 4.4. Using this option in
  92       *    other contexts will result in an exception at execution time.
  93       *
  94       *  * maxTimeMS (integer): The maximum amount of time to allow the query to
  95       *    run.
  96       *
  97       *  * new (boolean): When true, returns the modified document rather than
  98       *    the original. This option is ignored for remove operations. The
  99       *    The default is false.
 100       *
 101       *  * query (document): Query by which to filter documents.
 102       *
 103       *  * remove (boolean): When true, removes the matched document. This option
 104       *    cannot be true if the update option is set. The default is false.
 105       *
 106       *  * session (MongoDB\Driver\Session): Client session.
 107       *
 108       *  * sort (document): Determines which document the operation modifies if
 109       *    the query selects multiple documents.
 110       *
 111       *  * typeMap (array): Type map for BSON deserialization.
 112       *
 113       *  * update (document): Update or replacement to apply to the matched
 114       *    document. This option cannot be set if the remove option is true.
 115       *
 116       *  * upsert (boolean): When true, a new document is created if no document
 117       *    matches the query. This option is ignored for remove operations. The
 118       *    default is false.
 119       *
 120       *  * let (document): Map of parameter names and values. Values must be
 121       *    constant or closed expressions that do not reference document fields.
 122       *    Parameters can then be accessed as variables in an aggregate
 123       *    expression context (e.g. "$$var").
 124       *
 125       *  * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
 126       *
 127       * @param string $databaseName   Database name
 128       * @param string $collectionName Collection name
 129       * @param array  $options        Command options
 130       * @throws InvalidArgumentException for parameter/option parsing errors
 131       */
 132      public function __construct(string $databaseName, string $collectionName, array $options)
 133      {
 134          $options += ['remove' => false];
 135  
 136          if (isset($options['arrayFilters']) && ! is_array($options['arrayFilters'])) {
 137              throw InvalidArgumentException::invalidType('"arrayFilters" option', $options['arrayFilters'], 'array');
 138          }
 139  
 140          if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) {
 141              throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean');
 142          }
 143  
 144          if (isset($options['collation']) && ! is_array($options['collation']) && ! is_object($options['collation'])) {
 145              throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object');
 146          }
 147  
 148          if (isset($options['fields']) && ! is_array($options['fields']) && ! is_object($options['fields'])) {
 149              throw InvalidArgumentException::invalidType('"fields" option', $options['fields'], 'array or object');
 150          }
 151  
 152          if (isset($options['hint']) && ! is_string($options['hint']) && ! is_array($options['hint']) && ! is_object($options['hint'])) {
 153              throw InvalidArgumentException::invalidType('"hint" option', $options['hint'], ['string', 'array', 'object']);
 154          }
 155  
 156          if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) {
 157              throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
 158          }
 159  
 160          if (array_key_exists('new', $options) && ! is_bool($options['new'])) {
 161              throw InvalidArgumentException::invalidType('"new" option', $options['new'], 'boolean');
 162          }
 163  
 164          if (isset($options['query']) && ! is_array($options['query']) && ! is_object($options['query'])) {
 165              throw InvalidArgumentException::invalidType('"query" option', $options['query'], 'array or object');
 166          }
 167  
 168          if (! is_bool($options['remove'])) {
 169              throw InvalidArgumentException::invalidType('"remove" option', $options['remove'], 'boolean');
 170          }
 171  
 172          if (isset($options['session']) && ! $options['session'] instanceof Session) {
 173              throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class);
 174          }
 175  
 176          if (isset($options['sort']) && ! is_array($options['sort']) && ! is_object($options['sort'])) {
 177              throw InvalidArgumentException::invalidType('"sort" option', $options['sort'], 'array or object');
 178          }
 179  
 180          if (isset($options['typeMap']) && ! is_array($options['typeMap'])) {
 181              throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array');
 182          }
 183  
 184          if (isset($options['update']) && ! is_array($options['update']) && ! is_object($options['update'])) {
 185              throw InvalidArgumentException::invalidType('"update" option', $options['update'], 'array or object');
 186          }
 187  
 188          if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
 189              throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class);
 190          }
 191  
 192          if (array_key_exists('upsert', $options) && ! is_bool($options['upsert'])) {
 193              throw InvalidArgumentException::invalidType('"upsert" option', $options['upsert'], 'boolean');
 194          }
 195  
 196          if (isset($options['let']) && ! is_array($options['let']) && ! is_object($options['let'])) {
 197              throw InvalidArgumentException::invalidType('"let" option', $options['let'], 'array or object');
 198          }
 199  
 200          if (isset($options['bypassDocumentValidation']) && ! $options['bypassDocumentValidation']) {
 201              unset($options['bypassDocumentValidation']);
 202          }
 203  
 204          if (! (isset($options['update']) xor $options['remove'])) {
 205              throw new InvalidArgumentException('The "remove" option must be true or an "update" document must be specified, but not both');
 206          }
 207  
 208          if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
 209              unset($options['writeConcern']);
 210          }
 211  
 212          $this->databaseName = $databaseName;
 213          $this->collectionName = $collectionName;
 214          $this->options = $options;
 215      }
 216  
 217      /**
 218       * Execute the operation.
 219       *
 220       * @see Executable::execute()
 221       * @return array|object|null
 222       * @throws UnexpectedValueException if the command response was malformed
 223       * @throws UnsupportedException if hint or write concern is used and unsupported
 224       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 225       */
 226      public function execute(Server $server)
 227      {
 228          /* Server versions >= 4.2.0 raise errors for unsupported update options.
 229           * For previous versions, the CRUD spec requires a client-side error. */
 230          if (isset($this->options['hint']) && ! server_supports_feature($server, self::$wireVersionForUnsupportedOptionServerSideError)) {
 231              throw UnsupportedException::hintNotSupported();
 232          }
 233  
 234          /* CRUD spec requires a client-side error when using "hint" with an
 235           * unacknowledged write concern on an unsupported server. */
 236          if (
 237              isset($this->options['writeConcern']) && ! is_write_concern_acknowledged($this->options['writeConcern']) &&
 238              isset($this->options['hint']) && ! server_supports_feature($server, self::$wireVersionForHint)
 239          ) {
 240              throw UnsupportedException::hintNotSupported();
 241          }
 242  
 243          $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction();
 244          if ($inTransaction && isset($this->options['writeConcern'])) {
 245              throw UnsupportedException::writeConcernNotSupportedInTransaction();
 246          }
 247  
 248          $cursor = $server->executeWriteCommand($this->databaseName, new Command($this->createCommandDocument()), $this->createOptions());
 249  
 250          if (isset($this->options['typeMap'])) {
 251              $cursor->setTypeMap(create_field_path_type_map($this->options['typeMap'], 'value'));
 252          }
 253  
 254          $result = current($cursor->toArray());
 255  
 256          return is_object($result) ? ($result->value ?? null) : null;
 257      }
 258  
 259      /**
 260       * Returns the command document for this operation.
 261       *
 262       * @see Explainable::getCommandDocument()
 263       * @return array
 264       */
 265      public function getCommandDocument(Server $server)
 266      {
 267          return $this->createCommandDocument();
 268      }
 269  
 270      /**
 271       * Create the findAndModify command document.
 272       */
 273      private function createCommandDocument(): array
 274      {
 275          $cmd = ['findAndModify' => $this->collectionName];
 276  
 277          if ($this->options['remove']) {
 278              $cmd['remove'] = true;
 279          } else {
 280              if (isset($this->options['new'])) {
 281                  $cmd['new'] = $this->options['new'];
 282              }
 283  
 284              if (isset($this->options['upsert'])) {
 285                  $cmd['upsert'] = $this->options['upsert'];
 286              }
 287          }
 288  
 289          foreach (['collation', 'fields', 'let', 'query', 'sort'] as $option) {
 290              if (isset($this->options[$option])) {
 291                  $cmd[$option] = (object) $this->options[$option];
 292              }
 293          }
 294  
 295          if (isset($this->options['update'])) {
 296              $cmd['update'] = is_pipeline($this->options['update'])
 297                  ? $this->options['update']
 298                  : (object) $this->options['update'];
 299          }
 300  
 301          foreach (['arrayFilters', 'bypassDocumentValidation', 'comment', 'hint', 'maxTimeMS'] as $option) {
 302              if (isset($this->options[$option])) {
 303                  $cmd[$option] = $this->options[$option];
 304              }
 305          }
 306  
 307          return $cmd;
 308      }
 309  
 310      /**
 311       * Create options for executing the command.
 312       *
 313       * @see https://php.net/manual/en/mongodb-driver-server.executewritecommand.php
 314       */
 315      private function createOptions(): array
 316      {
 317          $options = [];
 318  
 319          if (isset($this->options['session'])) {
 320              $options['session'] = $this->options['session'];
 321          }
 322  
 323          if (isset($this->options['writeConcern'])) {
 324              $options['writeConcern'] = $this->options['writeConcern'];
 325          }
 326  
 327          return $options;
 328      }
 329  }