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\BulkWrite as Bulk;
  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\UnsupportedException;
  27  use MongoDB\UpdateResult;
  28  
  29  use function is_array;
  30  use function is_bool;
  31  use function is_object;
  32  use function is_string;
  33  use function MongoDB\is_first_key_operator;
  34  use function MongoDB\is_pipeline;
  35  use function MongoDB\is_write_concern_acknowledged;
  36  use function MongoDB\server_supports_feature;
  37  
  38  /**
  39   * Operation for the update command.
  40   *
  41   * This class is used internally by the ReplaceOne, UpdateMany, and UpdateOne
  42   * operation classes.
  43   *
  44   * @internal
  45   * @see https://mongodb.com/docs/manual/reference/command/update/
  46   */
  47  class Update implements Executable, Explainable
  48  {
  49      /** @var integer */
  50      private static $wireVersionForHint = 8;
  51  
  52      /** @var string */
  53      private $databaseName;
  54  
  55      /** @var string */
  56      private $collectionName;
  57  
  58      /** @var array|object */
  59      private $filter;
  60  
  61      /** @var array|object */
  62      private $update;
  63  
  64      /** @var array */
  65      private $options;
  66  
  67      /**
  68       * Constructs a update 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       *  * bypassDocumentValidation (boolean): If true, allows the write to
  76       *    circumvent document level validation.
  77       *
  78       *  * collation (document): Collation specification.
  79       *
  80       *  * comment (mixed): BSON value to attach as a comment to this command.
  81       *
  82       *    This is not supported for servers versions < 4.4.
  83       *
  84       *  * hint (string|document): The index to use. Specify either the index
  85       *    name as a string or the index key pattern as a document. If specified,
  86       *    then the query system will only consider plans using the hinted index.
  87       *
  88       *    This is not supported for server versions < 4.2 and will result in an
  89       *    exception at execution time if used.
  90       *
  91       *  * multi (boolean): When true, updates all documents matching the query.
  92       *    This option cannot be true if the $update argument is a replacement
  93       *    document (i.e. contains no update operators). The default is false.
  94       *
  95       *  * session (MongoDB\Driver\Session): Client session.
  96       *
  97       *  * upsert (boolean): When true, a new document is created if no document
  98       *    matches the query. The default is false.
  99       *
 100       *  * let (document): Map of parameter names and values. Values must be
 101       *    constant or closed expressions that do not reference document fields.
 102       *    Parameters can then be accessed as variables in an aggregate
 103       *    expression context (e.g. "$$var").
 104       *
 105       *  * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
 106       *
 107       * @param string       $databaseName   Database name
 108       * @param string       $collectionName Collection name
 109       * @param array|object $filter         Query by which to delete documents
 110       * @param array|object $update         Update to apply to the matched
 111       *                                     document(s) or a replacement document
 112       * @param array        $options        Command options
 113       * @throws InvalidArgumentException for parameter/option parsing errors
 114       */
 115      public function __construct(string $databaseName, string $collectionName, $filter, $update, array $options = [])
 116      {
 117          if (! is_array($filter) && ! is_object($filter)) {
 118              throw InvalidArgumentException::invalidType('$filter', $filter, 'array or object');
 119          }
 120  
 121          if (! is_array($update) && ! is_object($update)) {
 122              throw InvalidArgumentException::invalidType('$update', $filter, 'array or object');
 123          }
 124  
 125          $options += [
 126              'multi' => false,
 127              'upsert' => false,
 128          ];
 129  
 130          if (isset($options['arrayFilters']) && ! is_array($options['arrayFilters'])) {
 131              throw InvalidArgumentException::invalidType('"arrayFilters" option', $options['arrayFilters'], 'array');
 132          }
 133  
 134          if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) {
 135              throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean');
 136          }
 137  
 138          if (isset($options['collation']) && ! is_array($options['collation']) && ! is_object($options['collation'])) {
 139              throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object');
 140          }
 141  
 142          if (isset($options['hint']) && ! is_string($options['hint']) && ! is_array($options['hint']) && ! is_object($options['hint'])) {
 143              throw InvalidArgumentException::invalidType('"hint" option', $options['hint'], ['string', 'array', 'object']);
 144          }
 145  
 146          if (! is_bool($options['multi'])) {
 147              throw InvalidArgumentException::invalidType('"multi" option', $options['multi'], 'boolean');
 148          }
 149  
 150          if ($options['multi'] && ! is_first_key_operator($update) && ! is_pipeline($update)) {
 151              throw new InvalidArgumentException('"multi" option cannot be true if $update is a replacement document');
 152          }
 153  
 154          if (isset($options['session']) && ! $options['session'] instanceof Session) {
 155              throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class);
 156          }
 157  
 158          if (! is_bool($options['upsert'])) {
 159              throw InvalidArgumentException::invalidType('"upsert" option', $options['upsert'], 'boolean');
 160          }
 161  
 162          if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
 163              throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class);
 164          }
 165  
 166          if (isset($options['let']) && ! is_array($options['let']) && ! is_object($options['let'])) {
 167              throw InvalidArgumentException::invalidType('"let" option', $options['let'], 'array or object');
 168          }
 169  
 170          if (isset($options['bypassDocumentValidation']) && ! $options['bypassDocumentValidation']) {
 171              unset($options['bypassDocumentValidation']);
 172          }
 173  
 174          if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
 175              unset($options['writeConcern']);
 176          }
 177  
 178          $this->databaseName = $databaseName;
 179          $this->collectionName = $collectionName;
 180          $this->filter = $filter;
 181          $this->update = $update;
 182          $this->options = $options;
 183      }
 184  
 185      /**
 186       * Execute the operation.
 187       *
 188       * @see Executable::execute()
 189       * @return UpdateResult
 190       * @throws UnsupportedException if hint or write concern is used and unsupported
 191       * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
 192       */
 193      public function execute(Server $server)
 194      {
 195          /* CRUD spec requires a client-side error when using "hint" with an
 196           * unacknowledged write concern on an unsupported server. */
 197          if (
 198              isset($this->options['writeConcern']) && ! is_write_concern_acknowledged($this->options['writeConcern']) &&
 199              isset($this->options['hint']) && ! server_supports_feature($server, self::$wireVersionForHint)
 200          ) {
 201              throw UnsupportedException::hintNotSupported();
 202          }
 203  
 204          $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction();
 205          if ($inTransaction && isset($this->options['writeConcern'])) {
 206              throw UnsupportedException::writeConcernNotSupportedInTransaction();
 207          }
 208  
 209          $bulk = new Bulk($this->createBulkWriteOptions());
 210          $bulk->update($this->filter, $this->update, $this->createUpdateOptions());
 211  
 212          $writeResult = $server->executeBulkWrite($this->databaseName . '.' . $this->collectionName, $bulk, $this->createExecuteOptions());
 213  
 214          return new UpdateResult($writeResult);
 215      }
 216  
 217      /**
 218       * Returns the command document for this operation.
 219       *
 220       * @see Explainable::getCommandDocument()
 221       * @return array
 222       */
 223      public function getCommandDocument(Server $server)
 224      {
 225          $cmd = ['update' => $this->collectionName, 'updates' => [['q' => $this->filter, 'u' => $this->update] + $this->createUpdateOptions()]];
 226  
 227          if (isset($this->options['bypassDocumentValidation'])) {
 228              $cmd['bypassDocumentValidation'] = $this->options['bypassDocumentValidation'];
 229          }
 230  
 231          if (isset($this->options['writeConcern'])) {
 232              $cmd['writeConcern'] = $this->options['writeConcern'];
 233          }
 234  
 235          return $cmd;
 236      }
 237  
 238      /**
 239       * Create options for constructing the bulk write.
 240       *
 241       * @see https://php.net/manual/en/mongodb-driver-bulkwrite.construct.php
 242       */
 243      private function createBulkWriteOptions(): array
 244      {
 245          $options = [];
 246  
 247          foreach (['bypassDocumentValidation', 'comment'] as $option) {
 248              if (isset($this->options[$option])) {
 249                  $options[$option] = $this->options[$option];
 250              }
 251          }
 252  
 253          if (isset($this->options['let'])) {
 254              $options['let'] = (object) $this->options['let'];
 255          }
 256  
 257          return $options;
 258      }
 259  
 260      /**
 261       * Create options for executing the bulk write.
 262       *
 263       * @see https://php.net/manual/en/mongodb-driver-server.executebulkwrite.php
 264       */
 265      private function createExecuteOptions(): array
 266      {
 267          $options = [];
 268  
 269          if (isset($this->options['session'])) {
 270              $options['session'] = $this->options['session'];
 271          }
 272  
 273          if (isset($this->options['writeConcern'])) {
 274              $options['writeConcern'] = $this->options['writeConcern'];
 275          }
 276  
 277          return $options;
 278      }
 279  
 280      /**
 281       * Create options for the update command.
 282       *
 283       * Note that these options are different from the bulk write options, which
 284       * are created in createExecuteOptions().
 285       */
 286      private function createUpdateOptions(): array
 287      {
 288          $updateOptions = [
 289              'multi' => $this->options['multi'],
 290              'upsert' => $this->options['upsert'],
 291          ];
 292  
 293          foreach (['arrayFilters', 'hint'] as $option) {
 294              if (isset($this->options[$option])) {
 295                  $updateOptions[$option] = $this->options[$option];
 296              }
 297          }
 298  
 299          if (isset($this->options['collation'])) {
 300              $updateOptions['collation'] = (object) $this->options['collation'];
 301          }
 302  
 303          return $updateOptions;
 304      }
 305  }