Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 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 MongoDB\create_field_path_type_map;
  34  use function MongoDB\is_pipeline;
  35  use function MongoDB\server_supports_feature;
  36  
  37  /**
  38   * Operation for the findAndModify command.
  39   *
  40   * This class is used internally by the FindOneAndDelete, FindOneAndReplace, and
  41   * FindOneAndUpdate operation classes.
  42   *
  43   * @internal
  44   * @see http://docs.mongodb.org/manual/reference/command/findAndModify/
  45   */
  46  class FindAndModify implements Executable, Explainable
  47  {
  48      /** @var integer */
  49      private static $wireVersionForArrayFilters = 6;
  50  
  51      /** @var integer */
  52      private static $wireVersionForCollation = 5;
  53  
  54      /** @var integer */
  55      private static $wireVersionForDocumentLevelValidation = 4;
  56  
  57      /** @var integer */
  58      private static $wireVersionForWriteConcern = 4;
  59  
  60      /** @var string */
  61      private $databaseName;
  62  
  63      /** @var string */
  64      private $collectionName;
  65  
  66      /** @var array */
  67      private $options;
  68  
  69      /**
  70       * Constructs a findAndModify command.
  71       *
  72       * Supported options:
  73       *
  74       *  * arrayFilters (document array): A set of filters specifying to which
  75       *    array elements an update should apply.
  76       *
  77       *    This is not supported for server versions < 3.6 and will result in an
  78       *    exception at execution time if used.
  79       *
  80       *  * collation (document): Collation specification.
  81       *
  82       *    This is not supported for server versions < 3.4 and will result in an
  83       *    exception at execution time if used.
  84       *
  85       *  * bypassDocumentValidation (boolean): If true, allows the write to
  86       *    circumvent document level validation.
  87       *
  88       *    For servers < 3.2, this option is ignored as document level validation
  89       *    is not available.
  90       *
  91       *  * fields (document): Limits the fields to return for the matching
  92       *    document.
  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       *    Sessions are not supported for server versions < 3.6.
 109       *
 110       *  * sort (document): Determines which document the operation modifies if
 111       *    the query selects multiple documents.
 112       *
 113       *  * typeMap (array): Type map for BSON deserialization.
 114       *
 115       *  * update (document): Update or replacement to apply to the matched
 116       *    document. This option cannot be set if the remove option is true.
 117       *
 118       *  * upsert (boolean): When true, a new document is created if no document
 119       *    matches the query. This option is ignored for remove operations. The
 120       *    default is false.
 121       *
 122       *  * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
 123       *
 124       *    This is not supported for server versions < 3.2 and will result in an
 125       *    exception at execution time if used.
 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($databaseName, $collectionName, array $options)
 133      {
 134          $options += [
 135              'new' => false,
 136              'remove' => false,
 137              'upsert' => false,
 138          ];
 139  
 140          if (isset($options['arrayFilters']) && ! is_array($options['arrayFilters'])) {
 141              throw InvalidArgumentException::invalidType('"arrayFilters" option', $options['arrayFilters'], 'array');
 142          }
 143  
 144          if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) {
 145              throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean');
 146          }
 147  
 148          if (isset($options['collation']) && ! is_array($options['collation']) && ! is_object($options['collation'])) {
 149              throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object');
 150          }
 151  
 152          if (isset($options['fields']) && ! is_array($options['fields']) && ! is_object($options['fields'])) {
 153              throw InvalidArgumentException::invalidType('"fields" option', $options['fields'], 'array or 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 (! 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 (! is_bool($options['upsert'])) {
 193              throw InvalidArgumentException::invalidType('"upsert" option', $options['upsert'], 'boolean');
 194          }
 195  
 196          if (! (isset($options['update']) xor $options['remove'])) {
 197              throw new InvalidArgumentException('The "remove" option must be true or an "update" document must be specified, but not both');
 198          }
 199  
 200          if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
 201              unset($options['writeConcern']);
 202          }
 203  
 204          $this->databaseName = (string) $databaseName;
 205          $this->collectionName = (string) $collectionName;
 206          $this->options = $options;
 207      }
 208  
 209      /**
 210       * Execute the operation.
 211       *
 212       * @see Executable::execute()
 213       * @param Server $server
 214       * @return array|object|null
 215       * @throws UnexpectedValueException if the command response was malformed
 216       * @throws UnsupportedException if array filters, collation, or write concern is used and unsupported
 217       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 218       */
 219      public function execute(Server $server)
 220      {
 221          if (isset($this->options['arrayFilters']) && ! server_supports_feature($server, self::$wireVersionForArrayFilters)) {
 222              throw UnsupportedException::arrayFiltersNotSupported();
 223          }
 224  
 225          if (isset($this->options['collation']) && ! server_supports_feature($server, self::$wireVersionForCollation)) {
 226              throw UnsupportedException::collationNotSupported();
 227          }
 228  
 229          if (isset($this->options['writeConcern']) && ! server_supports_feature($server, self::$wireVersionForWriteConcern)) {
 230              throw UnsupportedException::writeConcernNotSupported();
 231          }
 232  
 233          $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction();
 234          if ($inTransaction && isset($this->options['writeConcern'])) {
 235              throw UnsupportedException::writeConcernNotSupportedInTransaction();
 236          }
 237  
 238          $cursor = $server->executeWriteCommand($this->databaseName, new Command($this->createCommandDocument($server)), $this->createOptions());
 239  
 240          if (isset($this->options['typeMap'])) {
 241              $cursor->setTypeMap(create_field_path_type_map($this->options['typeMap'], 'value'));
 242          }
 243  
 244          $result = current($cursor->toArray());
 245  
 246          return isset($result->value) ? $result->value : null;
 247      }
 248  
 249      public function getCommandDocument(Server $server)
 250      {
 251          return $this->createCommandDocument($server);
 252      }
 253  
 254      /**
 255       * Create the findAndModify command document.
 256       *
 257       * @param Server $server
 258       * @return array
 259       */
 260      private function createCommandDocument(Server $server)
 261      {
 262          $cmd = ['findAndModify' => $this->collectionName];
 263  
 264          if ($this->options['remove']) {
 265              $cmd['remove'] = true;
 266          } else {
 267              $cmd['new'] = $this->options['new'];
 268              $cmd['upsert'] = $this->options['upsert'];
 269          }
 270  
 271          foreach (['collation', 'fields', 'query', 'sort'] as $option) {
 272              if (isset($this->options[$option])) {
 273                  $cmd[$option] = (object) $this->options[$option];
 274              }
 275          }
 276  
 277          if (isset($this->options['update'])) {
 278              $cmd['update'] = is_pipeline($this->options['update'])
 279                  ? $this->options['update']
 280                  : (object) $this->options['update'];
 281          }
 282  
 283          if (isset($this->options['arrayFilters'])) {
 284              $cmd['arrayFilters'] = $this->options['arrayFilters'];
 285          }
 286  
 287          if (isset($this->options['maxTimeMS'])) {
 288              $cmd['maxTimeMS'] = $this->options['maxTimeMS'];
 289          }
 290  
 291          if (! empty($this->options['bypassDocumentValidation']) &&
 292              server_supports_feature($server, self::$wireVersionForDocumentLevelValidation)
 293          ) {
 294              $cmd['bypassDocumentValidation'] = $this->options['bypassDocumentValidation'];
 295          }
 296  
 297          return $cmd;
 298      }
 299  
 300      /**
 301       * Create options for executing the command.
 302       *
 303       * @see http://php.net/manual/en/mongodb-driver-server.executewritecommand.php
 304       * @return array
 305       */
 306      private function createOptions()
 307      {
 308          $options = [];
 309  
 310          if (isset($this->options['session'])) {
 311              $options['session'] = $this->options['session'];
 312          }
 313  
 314          if (isset($this->options['writeConcern'])) {
 315              $options['writeConcern'] = $this->options['writeConcern'];
 316          }
 317  
 318          return $options;
 319      }
 320  }