Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 39 and 400] [Versions 400 and 401]

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