See Release Notes
Long Term Support Release
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 ArrayIterator; 21 use MongoDB\BSON\JavascriptInterface; 22 use MongoDB\Driver\Command; 23 use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; 24 use MongoDB\Driver\ReadConcern; 25 use MongoDB\Driver\ReadPreference; 26 use MongoDB\Driver\Server; 27 use MongoDB\Driver\Session; 28 use MongoDB\Driver\WriteConcern; 29 use MongoDB\Exception\InvalidArgumentException; 30 use MongoDB\Exception\UnexpectedValueException; 31 use MongoDB\Exception\UnsupportedException; 32 use MongoDB\MapReduceResult; 33 use stdClass; 34 35 use function assert; 36 use function current; 37 use function is_array; 38 use function is_bool; 39 use function is_integer; 40 use function is_object; 41 use function is_string; 42 use function MongoDB\create_field_path_type_map; 43 use function MongoDB\is_mapreduce_output_inline; 44 use function trigger_error; 45 46 use const E_USER_DEPRECATED; 47 48 /** 49 * Operation for the mapReduce command. 50 * 51 * @api 52 * @see \MongoDB\Collection::mapReduce() 53 * @see https://mongodb.com/docs/manual/reference/command/mapReduce/ 54 */ 55 class MapReduce implements Executable 56 { 57 /** @var string */ 58 private $databaseName; 59 60 /** @var string */ 61 private $collectionName; 62 63 /** @var JavascriptInterface */ 64 private $map; 65 66 /** @var JavascriptInterface */ 67 private $reduce; 68 69 /** @var array|object|string */ 70 private $out; 71 72 /** @var array */ 73 private $options; 74 75 /** 76 * Constructs a mapReduce command. 77 * 78 * Required arguments: 79 * 80 * * map (MongoDB\BSON\Javascript): A JavaScript function that associates 81 * or "maps" a value with a key and emits the key and value pair. 82 * 83 * Passing a Javascript instance with a scope is deprecated. Put all 84 * scope variables in the "scope" option of the MapReduce operation. 85 * 86 * * reduce (MongoDB\BSON\Javascript): A JavaScript function that "reduces" 87 * to a single object all the values associated with a particular key. 88 * 89 * Passing a Javascript instance with a scope is deprecated. Put all 90 * scope variables in the "scope" option of the MapReduce operation. 91 * 92 * * out (string|document): Specifies where to output the result of the 93 * map-reduce operation. You can either output to a collection or return 94 * the result inline. On a primary member of a replica set you can output 95 * either to a collection or inline, but on a secondary, only inline 96 * output is possible. 97 * 98 * Supported options: 99 * 100 * * bypassDocumentValidation (boolean): If true, allows the write to 101 * circumvent document level validation. This only applies when results 102 * are output to a collection. 103 * 104 * * collation (document): Collation specification. 105 * 106 * * comment (mixed): BSON value to attach as a comment to this command. 107 * 108 * This is not supported for servers versions < 4.4. 109 * 110 * * finalize (MongoDB\BSON\JavascriptInterface): Follows the reduce method 111 * and modifies the output. 112 * 113 * Passing a Javascript instance with a scope is deprecated. Put all 114 * scope variables in the "scope" option of the MapReduce operation. 115 * 116 * * jsMode (boolean): Specifies whether to convert intermediate data into 117 * BSON format between the execution of the map and reduce functions. 118 * 119 * * limit (integer): Specifies a maximum number of documents for the input 120 * into the map function. 121 * 122 * * maxTimeMS (integer): The maximum amount of time to allow the query to 123 * run. 124 * 125 * * query (document): Specifies the selection criteria using query 126 * operators for determining the documents input to the map function. 127 * 128 * * readConcern (MongoDB\Driver\ReadConcern): Read concern. This is not 129 * supported when results are returned inline. 130 * 131 * * readPreference (MongoDB\Driver\ReadPreference): Read preference. 132 * 133 * This option is ignored if results are output to a collection. 134 * 135 * * scope (document): Specifies global variables that are accessible in 136 * the map, reduce and finalize functions. 137 * 138 * * session (MongoDB\Driver\Session): Client session. 139 * 140 * * sort (document): Sorts the input documents. This option is useful for 141 * optimization. For example, specify the sort key to be the same as the 142 * emit key so that there are fewer reduce operations. The sort key must 143 * be in an existing index for this collection. 144 * 145 * * typeMap (array): Type map for BSON deserialization. This will be 146 * applied to the returned Cursor (it is not sent to the server). 147 * 148 * * verbose (boolean): Specifies whether to include the timing information 149 * in the result information. 150 * 151 * * writeConcern (MongoDB\Driver\WriteConcern): Write concern. This only 152 * applies when results are output to a collection. 153 * 154 * @param string $databaseName Database name 155 * @param string $collectionName Collection name 156 * @param JavascriptInterface $map Map function 157 * @param JavascriptInterface $reduce Reduce function 158 * @param string|array|object $out Output specification 159 * @param array $options Command options 160 * @throws InvalidArgumentException for parameter/option parsing errors 161 */ 162 public function __construct(string $databaseName, string $collectionName, JavascriptInterface $map, JavascriptInterface $reduce, $out, array $options = []) 163 { 164 if (! is_string($out) && ! is_array($out) && ! is_object($out)) { 165 throw InvalidArgumentException::invalidType('$out', $out, 'string or array or object'); 166 } 167 168 if (isset($options['bypassDocumentValidation']) && ! is_bool($options['bypassDocumentValidation'])) { 169 throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $options['bypassDocumentValidation'], 'boolean'); 170 } 171 172 if (isset($options['collation']) && ! is_array($options['collation']) && ! is_object($options['collation'])) { 173 throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object'); 174 } 175 176 if (isset($options['finalize']) && ! $options['finalize'] instanceof JavascriptInterface) { 177 throw InvalidArgumentException::invalidType('"finalize" option', $options['finalize'], JavascriptInterface::class); 178 } 179 180 if (isset($options['jsMode']) && ! is_bool($options['jsMode'])) { 181 throw InvalidArgumentException::invalidType('"jsMode" option', $options['jsMode'], 'boolean'); 182 } 183 184 if (isset($options['limit']) && ! is_integer($options['limit'])) { 185 throw InvalidArgumentException::invalidType('"limit" option', $options['limit'], 'integer'); 186 } 187 188 if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) { 189 throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer'); 190 } 191 192 if (isset($options['query']) && ! is_array($options['query']) && ! is_object($options['query'])) { 193 throw InvalidArgumentException::invalidType('"query" option', $options['query'], 'array or object'); 194 } 195 196 if (isset($options['readConcern']) && ! $options['readConcern'] instanceof ReadConcern) { 197 throw InvalidArgumentException::invalidType('"readConcern" option', $options['readConcern'], ReadConcern::class); 198 } 199 200 if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) { 201 throw InvalidArgumentException::invalidType('"readPreference" option', $options['readPreference'], ReadPreference::class); 202 } 203 204 if (isset($options['scope']) && ! is_array($options['scope']) && ! is_object($options['scope'])) { 205 throw InvalidArgumentException::invalidType('"scope" option', $options['scope'], 'array or object'); 206 } 207 208 if (isset($options['session']) && ! $options['session'] instanceof Session) { 209 throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class); 210 } 211 212 if (isset($options['sort']) && ! is_array($options['sort']) && ! is_object($options['sort'])) { 213 throw InvalidArgumentException::invalidType('"sort" option', $options['sort'], 'array or object'); 214 } 215 216 if (isset($options['typeMap']) && ! is_array($options['typeMap'])) { 217 throw InvalidArgumentException::invalidType('"typeMap" option', $options['typeMap'], 'array'); 218 } 219 220 if (isset($options['verbose']) && ! is_bool($options['verbose'])) { 221 throw InvalidArgumentException::invalidType('"verbose" option', $options['verbose'], 'boolean'); 222 } 223 224 if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) { 225 throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class); 226 } 227 228 if (isset($options['bypassDocumentValidation']) && ! $options['bypassDocumentValidation']) { 229 unset($options['bypassDocumentValidation']); 230 } 231 232 if (isset($options['readConcern']) && $options['readConcern']->isDefault()) { 233 unset($options['readConcern']); 234 } 235 236 if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) { 237 unset($options['writeConcern']); 238 } 239 240 // Handle deprecation of CodeWScope 241 if ($map->getScope() !== null) { 242 @trigger_error('Use of Javascript with scope in "$map" argument for MapReduce is deprecated. Put all scope variables in the "scope" option of the MapReduce operation.', E_USER_DEPRECATED); 243 } 244 245 if ($reduce->getScope() !== null) { 246 @trigger_error('Use of Javascript with scope in "$reduce" argument for MapReduce is deprecated. Put all scope variables in the "scope" option of the MapReduce operation.', E_USER_DEPRECATED); 247 } 248 249 if (isset($options['finalize']) && $options['finalize']->getScope() !== null) { 250 @trigger_error('Use of Javascript with scope in "finalize" option for MapReduce is deprecated. Put all scope variables in the "scope" option of the MapReduce operation.', E_USER_DEPRECATED); 251 } 252 253 $this->checkOutDeprecations($out); 254 255 $this->databaseName = $databaseName; 256 $this->collectionName = $collectionName; 257 $this->map = $map; 258 $this->reduce = $reduce; 259 $this->out = $out; 260 $this->options = $options; 261 } 262 263 /** 264 * Execute the operation. 265 * 266 * @see Executable::execute() 267 * @return MapReduceResult 268 * @throws UnexpectedValueException if the command response was malformed 269 * @throws UnsupportedException if read concern or write concern is used and unsupported 270 * @throws DriverRuntimeException for other driver errors (e.g. connection errors) 271 */ 272 public function execute(Server $server) 273 { 274 $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction(); 275 if ($inTransaction) { 276 if (isset($this->options['readConcern'])) { 277 throw UnsupportedException::readConcernNotSupportedInTransaction(); 278 } 279 280 if (isset($this->options['writeConcern'])) { 281 throw UnsupportedException::writeConcernNotSupportedInTransaction(); 282 } 283 } 284 285 $hasOutputCollection = ! is_mapreduce_output_inline($this->out); 286 287 $command = $this->createCommand(); 288 $options = $this->createOptions($hasOutputCollection); 289 290 /* If the mapReduce operation results in a write, use 291 * executeReadWriteCommand to ensure we're handling the writeConcern 292 * option. 293 * In other cases, we use executeCommand as this will prevent the 294 * mapReduce operation from being retried when retryReads is enabled. 295 * See https://github.com/mongodb/specifications/blob/master/source/retryable-reads/retryable-reads.rst#unsupported-read-operations. */ 296 $cursor = $hasOutputCollection 297 ? $server->executeReadWriteCommand($this->databaseName, $command, $options) 298 : $server->executeCommand($this->databaseName, $command, $options); 299 300 if (isset($this->options['typeMap']) && ! $hasOutputCollection) { 301 $cursor->setTypeMap(create_field_path_type_map($this->options['typeMap'], 'results.$')); 302 } 303 304 $result = current($cursor->toArray()); 305 assert($result instanceof stdClass); 306 307 $getIterator = $this->createGetIteratorCallable($result, $server); 308 309 return new MapReduceResult($getIterator, $result); 310 } 311 312 /** 313 * @param string|array|object $out 314 */ 315 private function checkOutDeprecations($out): void 316 { 317 if (is_string($out)) { 318 return; 319 } 320 321 $out = (array) $out; 322 323 if (isset($out['nonAtomic']) && ! $out['nonAtomic']) { 324 @trigger_error('Specifying false for "out.nonAtomic" is deprecated.', E_USER_DEPRECATED); 325 } 326 327 if (isset($out['sharded']) && ! $out['sharded']) { 328 @trigger_error('Specifying false for "out.sharded" is deprecated.', E_USER_DEPRECATED); 329 } 330 } 331 332 /** 333 * Create the mapReduce command. 334 */ 335 private function createCommand(): Command 336 { 337 $cmd = [ 338 'mapReduce' => $this->collectionName, 339 'map' => $this->map, 340 'reduce' => $this->reduce, 341 'out' => $this->out, 342 ]; 343 344 foreach (['bypassDocumentValidation', 'comment', 'finalize', 'jsMode', 'limit', 'maxTimeMS', 'verbose'] as $option) { 345 if (isset($this->options[$option])) { 346 $cmd[$option] = $this->options[$option]; 347 } 348 } 349 350 foreach (['collation', 'query', 'scope', 'sort'] as $option) { 351 if (isset($this->options[$option])) { 352 $cmd[$option] = (object) $this->options[$option]; 353 } 354 } 355 356 return new Command($cmd); 357 } 358 359 /** 360 * Creates a callable for MapReduceResult::getIterator(). 361 * 362 * @throws UnexpectedValueException if the command response was malformed 363 */ 364 private function createGetIteratorCallable(stdClass $result, Server $server): callable 365 { 366 // Inline results can be wrapped with an ArrayIterator 367 if (isset($result->results) && is_array($result->results)) { 368 $results = $result->results; 369 370 return function () use ($results) { 371 return new ArrayIterator($results); 372 }; 373 } 374 375 if (isset($result->result) && (is_string($result->result) || is_object($result->result))) { 376 $options = isset($this->options['typeMap']) ? ['typeMap' => $this->options['typeMap']] : []; 377 378 $find = is_string($result->result) 379 ? new Find($this->databaseName, $result->result, [], $options) 380 : new Find($result->result->db, $result->result->collection, [], $options); 381 382 return function () use ($find, $server) { 383 return $find->execute($server); 384 }; 385 } 386 387 throw new UnexpectedValueException('mapReduce command did not return inline results or an output collection'); 388 } 389 390 /** 391 * Create options for executing the command. 392 * 393 * @see https://php.net/manual/en/mongodb-driver-server.executereadcommand.php 394 * @see https://php.net/manual/en/mongodb-driver-server.executereadwritecommand.php 395 */ 396 private function createOptions(bool $hasOutputCollection): array 397 { 398 $options = []; 399 400 if (isset($this->options['readConcern'])) { 401 $options['readConcern'] = $this->options['readConcern']; 402 } 403 404 if (! $hasOutputCollection && isset($this->options['readPreference'])) { 405 $options['readPreference'] = $this->options['readPreference']; 406 } 407 408 if (isset($this->options['session'])) { 409 $options['session'] = $this->options['session']; 410 } 411 412 if ($hasOutputCollection && isset($this->options['writeConcern'])) { 413 $options['writeConcern'] = $this->options['writeConcern']; 414 } 415 416 return $options; 417 } 418 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body