<?php
/*
< * Copyright 2015-2017 MongoDB, Inc.
> * Copyright 2015-present 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
> * https://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\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Driver\Manager;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\Server;
use MongoDB\Driver\Session;
> use MongoDB\Driver\WriteConcern;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\RuntimeException;
> use MongoDB\Operation\ListCollections;
use MongoDB\Operation\WithTransaction;
use ReflectionClass;
use ReflectionException;
>
use function end;
> use function assert;
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;
/**
> * Check whether all servers support executing a write stage on a secondary.
* Applies a type map to a document.
> *
*
> * @internal
* This function is used by operations where it is not possible to apply a type
> * @param Server[] $servers
* map to the cursor directly because the root document is a command response
> */
* (e.g. findAndModify).
> function all_servers_support_write_stage_on_secondary(array $servers): bool
*
> {
* @internal
> /* Write stages on secondaries are technically supported by FCV 4.4, but the
* @param array|object $document Document to which the type map will be applied
> * CRUD spec requires all 5.0+ servers since FCV is not tracked by SDAM. */
* @param array $typeMap Type map for BSON deserialization.
> static $wireVersionForWriteStageOnSecondary = 13;
* @return array|object
>
* @throws InvalidArgumentException
> foreach ($servers as $server) {
*/
> // We can assume that load balancers only front 5.0+ servers
function apply_type_map_to_document($document, array $typeMap)
> if ($server->getType() === Server::TYPE_LOAD_BALANCER) {
{
> continue;
if (! is_array($document) && ! is_object($document)) {
> }
throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
>
}
> if (! server_supports_feature($server, $wireVersionForWriteStageOnSecondary)) {
> return false;
return toPHP(fromPHP($document), $typeMap);
> }
}
> }
>
/**
> return true;
* 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)
> function generate_index_name($document): string
{
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 a collection's encryptedFields from the encryptedFieldsMap
* Return whether the first key in the document starts with a "$" character.
> * autoEncryption driver option (if available).
*
> *
* This is used for differentiating update and replacement documents.
> * @internal
*
> * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#drop-collection-helper
* @internal
> * @see Collection::drop
* @param array|object $document Update or replacement document
> * @see Database::createCollection
* @return boolean
> * @see Database::dropCollection
* @throws InvalidArgumentException
> * @return array|object|null
*/
> */
function is_first_key_operator($document)
> function get_encrypted_fields_from_driver(string $databaseName, string $collectionName, Manager $manager)
{
> {
if ($document instanceof Serializable) {
> $encryptedFieldsMap = (array) $manager->getEncryptedFieldsMap();
$document = $document->bsonSerialize();
>
}
> return $encryptedFieldsMap[$databaseName . '.' . $collectionName] ?? null;
> }
if (is_object($document)) {
>
$document = get_object_vars($document);
> /**
}
> * Return a collection's encryptedFields option from the server (if any).
> *
if (! is_array($document)) {
> * @internal
throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
> * @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#drop-collection-helper
}
> * @see Collection::drop
> * @see Database::dropCollection
reset($document);
> * @return array|object|null
$firstKey = (string) key($document);
> */
> function get_encrypted_fields_from_server(string $databaseName, string $collectionName, Manager $manager, Server $server)
return isset($firstKey[0]) && $firstKey[0] === '$';
> {
}
> // No-op if the encryptedFieldsMap autoEncryption driver option was omitted
> if ($manager->getEncryptedFieldsMap() === null) {
/**
> return null;
* Returns whether an update specification is a valid aggregation pipeline.
> }
*
>
* @internal
> $collectionInfoIterator = (new ListCollections($databaseName, ['filter' => ['name' => $collectionName]]))->execute($server);
* @param mixed $pipeline
>
* @return boolean
> foreach ($collectionInfoIterator as $collectionInfo) {
*/
> /* Note: ListCollections applies a typeMap that converts BSON documents
function is_pipeline($pipeline)
> * to PHP arrays. This should not be problematic as encryptedFields here
{
> * is only used by drop helpers to obtain names of supporting encryption
if (! is_array($pipeline)) {
> * collections. */
return false;
> return $collectionInfo['options']['encryptedFields'] ?? null;
}
> }
>
if ($pipeline === []) {
> return null;
return false;
> }
}
>
> /**
< * @return boolean
< function is_first_key_operator($document)
> function is_first_key_operator($document): bool
< * @return boolean
< function is_pipeline($pipeline)
> function is_pipeline($pipeline): bool
return false;
}
if ($expectedKey !== $key) {
return false;
}
$expectedKey++;
$stage = (array) $stage;
reset($stage);
$key = key($stage);
< if (! isset($key[0]) || $key[0] !== '$') {
> if (! is_string($key) || substr($key, 0, 1) !== '$') {
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)
> function is_in_transaction(array $options): bool
{
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)
> function is_last_pipeline_operator_write(array $pipeline): bool
{
$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
> * @see https://mongodb.com/docs/manual/reference/command/mapReduce/#output-inline
* @param string|array|object $out Output specification
< * @return boolean
* @throws InvalidArgumentException
*/
< function is_mapreduce_output_inline($out)
> function is_mapreduce_output_inline($out): bool
{
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 write concern is acknowledged.
* Return whether the server supports a particular feature.
> *
*
> * This function is similar to mongoc_write_concern_is_acknowledged but does not
* @internal
> * check the fsync option since that was never supported in the PHP driver.
* @param Server $server Server to check
> *
* @param integer $feature Feature constant (i.e. wire protocol version)
> * @internal
* @return boolean
> * @see https://mongodb.com/docs/manual/reference/write-concern/
*/
> */
function server_supports_feature(Server $server, $feature)
> function is_write_concern_acknowledged(WriteConcern $writeConcern): bool
{
> {
$info = $server->getInfo();
> /* Note: -1 corresponds to MONGOC_WRITE_CONCERN_W_ERRORS_IGNORED, which is
$maxWireVersion = isset($info['maxWireVersion']) ? (integer) $info['maxWireVersion'] : 0;
> * deprecated synonym of MONGOC_WRITE_CONCERN_W_UNACKNOWLEDGED and slated
$minWireVersion = isset($info['minWireVersion']) ? (integer) $info['minWireVersion'] : 0;
> * for removal in libmongoc 2.0. */
> return ($writeConcern->getW() !== 0 && $writeConcern->getW() !== -1) || $writeConcern->getJournal() === true;
return $minWireVersion <= $feature && $maxWireVersion >= $feature;
> }
}
>
> /**
< * @return boolean
< function server_supports_feature(Server $server, $feature)
> function server_supports_feature(Server $server, int $feature): bool
< function is_string_array($input)
> /**
> * Return whether the input is an array of strings.
> *
> * @internal
> * @param mixed $input
> */
> function is_string_array($input): bool
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)
> function create_field_path_type_map(array $typeMap, string $fieldPath): array
{
// 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 = [])
> function with_transaction(Session $session, callable $callback, array $transactionOptions = []): void
{
$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)
> function extract_session_from_options(array $options): ?Session
{
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)
> function extract_read_preference_from_options(array $options): ?ReadPreference
{
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)
> function select_server(Manager $manager, array $options): Server
{
$session = extract_session_from_options($options);
< if ($session instanceof Session && $session->getServer() !== null) {
< return $session->getServer();
> $server = $session instanceof Session ? $session->getServer() : null;
> if ($server !== null) {
> return $server;
}
$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);
> }
}
>
> /**
> * Performs server selection for an aggregate operation with a write stage. The
> * $options parameter may be modified by reference if a primary read preference
> * must be forced due to the existence of pre-5.0 servers in the topology.
> *
> * @internal
> * @see https://github.com/mongodb/specifications/blob/master/source/crud/crud.rst#aggregation-pipelines-with-write-stages
> */
> function select_server_for_aggregate_write_stage(Manager $manager, array &$options): Server
> {
> $readPreference = extract_read_preference_from_options($options);
>
> /* If there is either no read preference or a primary read preference, there
> * is no special server selection logic to apply. */
> if ($readPreference === null || $readPreference->getMode() === ReadPreference::RP_PRIMARY) {
> return select_server($manager, $options);
> }
>
> $server = null;
> $serverSelectionError = null;
>
> try {
> $server = select_server($manager, $options);
> } catch (DriverRuntimeException $serverSelectionError) {
> }
>
> /* If any pre-5.0 servers exist in the topology, force a primary read
> * preference and repeat server selection if it previously failed or
> * selected a secondary. */
> if (! all_servers_support_write_stage_on_secondary($manager->getServers())) {
> $options['readPreference'] = new ReadPreference(ReadPreference::RP_PRIMARY);
>
> if ($server === null || $server->isSecondary()) {
> return select_server($manager, $options);
> }
> }
>
> /* If the topology only contains 5.0+ servers, we should either return the
> * previously selected server or propagate the server selection error. */
> if ($serverSelectionError !== null) {
> throw $serverSelectionError;
> }
>
> assert($server instanceof Server);
>
> return $server;