1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
<?php
/*
* Copyright 2015-2017 MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace MongoDB;
use Exception;
use MongoDB\BSON\Serializable;
use MongoDB\Driver\Manager;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\Server;
use MongoDB\Driver\Session;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\RuntimeException;
use MongoDB\Operation\WithTransaction;
use ReflectionClass;
use ReflectionException;
use function end;
use function get_object_vars;
use function in_array;
use function is_array;
use function is_object;
use function is_string;
use function key;
use function MongoDB\BSON\fromPHP;
use function MongoDB\BSON\toPHP;
use function reset;
use function substr;
/**
* Applies a type map to a document.
*
* This function is used by operations where it is not possible to apply a type
* map to the cursor directly because the root document is a command response
* (e.g. findAndModify).
*
* @internal
* @param array|object $document Document to which the type map will be applied
* @param array $typeMap Type map for BSON deserialization.
* @return array|object
* @throws InvalidArgumentException
*/
function apply_type_map_to_document($document, array $typeMap)
{
if (! is_array($document) && ! is_object($document)) {
throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
}
return toPHP(fromPHP($document), $typeMap);
}
/**
* Generate an index name from a key specification.
*
* @internal
* @param array|object $document Document containing fields mapped to values,
* which denote order or an index type
* @return string
* @throws InvalidArgumentException
*/
function generate_index_name($document)
{
if ($document instanceof Serializable) {
$document = $document->bsonSerialize();
}
if (is_object($document)) {
$document = get_object_vars($document);
}
if (! is_array($document)) {
throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
}
$name = '';
foreach ($document as $field => $type) {
$name .= ($name != '' ? '_' : '') . $field . '_' . $type;
}
return $name;
}
/**
* Return whether the first key in the document starts with a "$" character.
*
* This is used for differentiating update and replacement documents.
*
* @internal
* @param array|object $document Update or replacement document
* @return boolean
* @throws InvalidArgumentException
*/
function is_first_key_operator($document)
{
if ($document instanceof Serializable) {
$document = $document->bsonSerialize();
}
if (is_object($document)) {
$document = get_object_vars($document);
}
if (! is_array($document)) {
throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
}
reset($document);
$firstKey = (string) key($document);
return isset($firstKey[0]) && $firstKey[0] === '$';
}
/**
* Returns whether an update specification is a valid aggregation pipeline.
*
* @internal
* @param mixed $pipeline
* @return boolean
*/
function is_pipeline($pipeline)
{
if (! is_array($pipeline)) {
return false;
}
if ($pipeline === []) {
return false;
}
$expectedKey = 0;
foreach ($pipeline as $key => $stage) {
if (! is_array($stage) && ! is_object($stage)) {
return false;
}
if ($expectedKey !== $key) {
return false;
}
$expectedKey++;
$stage = (array) $stage;
reset($stage);
$key = key($stage);
if (! isset($key[0]) || $key[0] !== '$') {
return false;
}
}
return true;
}
/**
* Returns whether we are currently in a transaction.
*
* @internal
* @param array $options Command options
* @return boolean
*/
function is_in_transaction(array $options)
{
if (isset($options['session']) && $options['session'] instanceof Session && $options['session']->isInTransaction()) {
return true;
}
return false;
}
/**
* Return whether the aggregation pipeline ends with an $out or $merge operator.
*
* This is used for determining whether the aggregation pipeline must be
* executed against a primary server.
*
* @internal
* @param array $pipeline List of pipeline operations
* @return boolean
*/
function is_last_pipeline_operator_write(array $pipeline)
{
$lastOp = end($pipeline);
if ($lastOp === false) {
return false;
}
$lastOp = (array) $lastOp;
return in_array(key($lastOp), ['$out', '$merge'], true);
}
/**
* Return whether the "out" option for a mapReduce operation is "inline".
*
* This is used to determine if a mapReduce command requires a primary.
*
* @internal
* @see https://docs.mongodb.com/manual/reference/command/mapReduce/#output-inline
* @param string|array|object $out Output specification
* @return boolean
* @throws InvalidArgumentException
*/
function is_mapreduce_output_inline($out)
{
if (! is_array($out) && ! is_object($out)) {
return false;
}
if ($out instanceof Serializable) {
$out = $out->bsonSerialize();
}
if (is_object($out)) {
$out = get_object_vars($out);
}
if (! is_array($out)) {
throw InvalidArgumentException::invalidType('$out', $out, 'array or object');
}
reset($out);
return key($out) === 'inline';
}
/**
* Return whether the server supports a particular feature.
*
* @internal
* @param Server $server Server to check
* @param integer $feature Feature constant (i.e. wire protocol version)
* @return boolean
*/
function server_supports_feature(Server $server, $feature)
{
$info = $server->getInfo();
$maxWireVersion = isset($info['maxWireVersion']) ? (integer) $info['maxWireVersion'] : 0;
$minWireVersion = isset($info['minWireVersion']) ? (integer) $info['minWireVersion'] : 0;
return $minWireVersion <= $feature && $maxWireVersion >= $feature;
}
function is_string_array($input)
{
if (! is_array($input)) {
return false;
}
foreach ($input as $item) {
if (! is_string($item)) {
return false;
}
}
return true;
}
/**
* Performs a deep copy of a value.
*
* This function will clone objects and recursively copy values within arrays.
*
* @internal
* @see https://bugs.php.net/bug.php?id=49664
* @param mixed $element Value to be copied
* @return mixed
* @throws ReflectionException
*/
function recursive_copy($element)
{
if (is_array($element)) {
foreach ($element as $key => $value) {
$element[$key] = recursive_copy($value);
}
return $element;
}
if (! is_object($element)) {
return $element;
}
if (! (new ReflectionClass($element))->isCloneable()) {
return $element;
}
return clone $element;
}
/**
* Creates a type map to apply to a field type
*
* This is used in the Aggregate, Distinct, and FindAndModify operations to
* apply the root-level type map to the document that will be returned. It also
* replaces the root type with object for consistency within these operations
*
* An existing type map for the given field path will not be overwritten
*
* @internal
* @param array $typeMap The existing typeMap
* @param string $fieldPath The field path to apply the root type to
* @return array
*/
function create_field_path_type_map(array $typeMap, $fieldPath)
{
// If some field paths already exist, we prefix them with the field path we are assuming as the new root
if (isset($typeMap['fieldPaths']) && is_array($typeMap['fieldPaths'])) {
$fieldPaths = $typeMap['fieldPaths'];
$typeMap['fieldPaths'] = [];
foreach ($fieldPaths as $existingFieldPath => $type) {
$typeMap['fieldPaths'][$fieldPath . '.' . $existingFieldPath] = $type;
}
}
// If a root typemap was set, apply this to the field object
if (isset($typeMap['root'])) {
$typeMap['fieldPaths'][$fieldPath] = $typeMap['root'];
}
/* Special case if we want to convert an array, in which case we need to
* ensure that the field containing the array is exposed as an array,
* instead of the type given in the type map's array key. */
if (substr($fieldPath, -2, 2) === '.$') {
$typeMap['fieldPaths'][substr($fieldPath, 0, -2)] = 'array';
}
$typeMap['root'] = 'object';
return $typeMap;
}
/**
* Execute a callback within a transaction in the given session
*
* This helper takes care of retrying the commit operation or the entire
* transaction if an error occurs.
*
* If the commit fails because of an UnknownTransactionCommitResult error, the
* commit is retried without re-invoking the callback.
* If the commit fails because of a TransientTransactionError, the entire
* transaction will be retried. In this case, the callback will be invoked
* again. It is important that the logic inside the callback is idempotent.
*
* In case of failures, the commit or transaction are retried until 120 seconds
* from the initial call have elapsed. After that, no retries will happen and
* the helper will throw the last exception received from the driver.
*
* @see Client::startSession
* @see Session::startTransaction for supported transaction options
*
* @param Session $session A session object as retrieved by Client::startSession
* @param callable $callback A callback that will be invoked within the transaction
* @param array $transactionOptions Additional options that are passed to Session::startTransaction
* @return void
* @throws RuntimeException for driver errors while committing the transaction
* @throws Exception for any other errors, including those thrown in the callback
*/
function with_transaction(Session $session, callable $callback, array $transactionOptions = [])
{
$operation = new WithTransaction($callback, $transactionOptions);
$operation->execute($session);
}
/**
* Returns the session option if it is set and valid.
*
* @internal
* @param array $options
* @return Session|null
*/
function extract_session_from_options(array $options)
{
if (! isset($options['session']) || ! $options['session'] instanceof Session) {
return null;
}
return $options['session'];
}
/**
* Returns the readPreference option if it is set and valid.
*
* @internal
* @param array $options
* @return ReadPreference|null
*/
function extract_read_preference_from_options(array $options)
{
if (! isset($options['readPreference']) || ! $options['readPreference'] instanceof ReadPreference) {
return null;
}
return $options['readPreference'];
}
/**
* Performs server selection, respecting the readPreference and session options
* (if given)
*
* @internal
* @return Server
*/
function select_server(Manager $manager, array $options)
{
$session = extract_session_from_options($options);
if ($session instanceof Session && $session->getServer() !== null) {
return $session->getServer();
}
$readPreference = extract_read_preference_from_options($options);
if (! $readPreference instanceof ReadPreference) {
// TODO: PHPLIB-476: Read transaction read preference once PHPC-1439 is implemented
$readPreference = new ReadPreference(ReadPreference::RP_PRIMARY);
}
return $manager->selectServer($readPreference);
}