You are here

mongodb.module in MongoDB 7

Same filename and directory in other branches
  1. 8 mongodb.module
  2. 6 mongodb.module

Contains the main module connecting Drupal to MongoDB.

File

mongodb.module
View source
<?php

/**
 * @file
 * Contains the main module connecting Drupal to MongoDB.
 */

/**
 * Implements hook_help().
 */
function mongodb_help($path, $arg) {
  switch ($path) {
    case 'admin/help#mongodb':
      return '<p>' . t('<a href="!project">MongoDB</a> implements a generic <a href="!mongo">MongoDB</a> interface.', array(
        '!project' => 'http://drupal.org/project/mongodb',
        '!mongo' => 'http://www.mongodb.org/',
      ));
  }
}

/**
 * Returns an MongoDB object.
 *
 * @param string $alias
 *   The name of a MongoDB connection alias. If it is not passed, or if the
 *   alias does not match a connection definition, the function will fall back
 *   to the 'default' alias.
 * @param int $retry
 *   The number of retry attemps when a connection fails.
 *
 * @return \MongoDB|\MongodbDummy
 *   A MongodbDummy is returned in case a MongoConnectionException is thrown.
 *
 * @throws \MongoConnectionException
 *   If the connection cannot be estaslished even after retries.
 * @throws \InvalidArgumentException
 *   If the database cannot be selected.
 * @throws \MongoConnectionException
 *   If the connection cannot be established.
 *
 * @see MongodbDummy
 */
function mongodb($alias = 'default', $retry = 3) {
  static $mongo_objects;
  $connections = variable_get('mongodb_connections', array());
  if (!isset($connections[$alias])) {
    $alias = 'default';
  }
  $connection = isset($connections[$alias]) ? $connections[$alias] : array();
  $connection += array(
    'host' => 'localhost',
    'db' => 'drupal',
    'connection_options' => array(),
  );
  $host = $connection['host'];
  $db = $connection['db'];
  $options = $connection['connection_options'] + array(
    'connect' => TRUE,
    'db' => $db,
  );
  if (!isset($mongo_objects[$host][$db])) {
    try {

      // Use the 1.3 client if available.
      if (class_exists(MongoClient::class)) {
        $mongo = new MongoClient($host, $options);

        // Enable read preference and tags if provided. This can also be
        // controlled on a per query basis at the cursor level if more control
        // is required.
        if (!empty($connection['read_preference'])) {
          $tags = !empty($connection['read_preference']['tags']) ? $connection['read_preference']['tags'] : array();
          $mongo
            ->setReadPreference($connection['read_preference']['preference'], $tags);
        }
      }
      else {
        $mongo = new Mongo($host, $options);
        if (!empty($connection['slave_ok'])) {
          $mongo
            ->setSlaveOkay(TRUE);
        }
      }
      $mongo_objects[$host][$db] = $mongo
        ->selectDB($db);
      $mongo_objects[$host][$db]->connection = $mongo;
    } catch (MongoConnectionException $e) {
      if ($retry > 0) {
        return mongodb($alias, --$retry);
      }
      $mongo_objects[$host][$db] = new MongodbDummy();
      throw $e;
    }
  }
  return $mongo_objects[$host][$db];
}

/**
 * Returns a MongoCollection object.
 *
 * @param mixed $collection_name
 *   Can be either a plain collection name or 0-based array containing a
 *   collection name and a prefix.
 *
 * @return \MongoCollection|\MongoDebugCollection|\MongodbDummy
 *   Return a MongoCollection in normal situations, a MongoDebugCollection if
 *   mongodb_debug is enabled, or a MongodbDummy if the MongoDB connection could
 *   not be established.
 *
 * @throws \InvalidArgumentException
 *   If the database cannot be selected.
 * @throws \MongoConnectionException
 *   If the connection cannot be established.
 */

// phpcs:ignore
function mongodb_collection() {
  $args = array_filter(func_get_args());
  if (is_array($args[0])) {
    list($collection_name, $prefixed) = $args[0];
    $prefixed .= $collection_name;
  }
  else {

    // Avoid something. collection names if NULLs are passed in.
    $collection_name = implode('.', array_filter($args));
    $prefixed = mongodb_collection_name($collection_name);
  }
  $collections = variable_get('mongodb_collections', array());
  if (isset($collections[$collection_name])) {

    // We might be dealing with an array or string because of need to preserve
    // backwards compatibility.
    $alias = is_array($collections[$collection_name]) && !empty($collections[$collection_name]['db_connection']) ? $collections[$collection_name]['db_connection'] : $collections[$collection_name];
  }
  else {
    $alias = 'default';
  }

  // Prefix the collection name for simpletest. It will be in the same DB as the
  // non-prefixed version so it's enough to prefix after choosing the mongodb
  // object.
  $mongodb_object = mongodb($alias);
  $collection = $mongodb_object
    ->selectCollection(mongodb_collection_name($collection_name));

  // Enable read preference and tags at a collection level if we have 1.3
  // client.
  if (!empty($collections[$alias]['read_preference']) && get_class($mongodb_object->connection) == 'MongoClient') {
    $tags = !empty($collections[$alias]['read_preference']['tags']) ? $collections[$alias]['read_preference']['tags'] : array();
    $collection
      ->setReadPreference($collections[$alias]['read_preference']['preference'], $tags);
  }
  $collection->connection = $mongodb_object->connection;
  return variable_get('mongodb_debug', FALSE) ? new MongoDebugCollection($collection) : $collection;
}

/**
 * Class MongoDebugCollection is a debug decorator for MongoCollection.
 */

// phpcs:ignore
class MongoDebugCollection {

  /**
   * Constructor.
   *
   * @param string $collection
   *   The collection to decorate.
   */
  public function __construct($collection) {
    $this->collection = $collection;
  }

  /**
   * A decorator for the MongoCollection::find() method adding debug info.
   *
   * @param array|null $query
   *   A MongoCollection;:find()-compatible query array.
   * @param array|null $fields
   *   A MongoCollection;:find()-compatible fields array.
   *
   * @return \MongoDebugCursor
   *   A debug cursor wrapping the decorated find() results.
   */
  public function find($query = array(), $fields = array()) {
    debug('find');
    debug($query);
    debug($fields);
    return new MongoDebugCursor($this->collection
      ->find($query, $fields));
  }

  /**
   * Decorates the standard __call() by debug()-ing its arguments.
   *
   * @param string $name
   *   The name of the called method.
   * @param array $arguments
   *   The arguments for the decorated __call().
   *
   * @return mixed
   *   The result of the decorated __call().
   */
  public function __call($name, array $arguments) {
    debug($name);
    debug($arguments);
    return call_user_func_array(array(
      $this->collection,
      $name,
    ), $arguments);
  }

}

/**
 * Class MongoDebugCursor is a debug decorator for MongoCollection::find().
 */

// phpcs:ignore
class MongoDebugCursor {

  /**
   * Constructor.
   *
   * @param string $collection
   *   The name of the collection on which the cursor applies.
   *
   * @see mongoDebugCollection::find()
   */
  public function __construct($collection) {
    $this->collection = $collection;
  }

  /**
   * Decorates the standard __call() by debug()-ing its arguments.
   *
   * @param string $name
   *   The name of the called method.
   * @param array $arguments
   *   The arguments for the decorated __call().
   *
   * @return mixed
   *   The result of the decorated __call().
   */
  public function __call($name, array $arguments) {
    debug($name);
    debug($arguments);
    return call_user_func_array(array(
      $this->collection,
      $name,
    ), $arguments);
  }

}

/**
 * Class MongodbDummy is a fake object accepting any method and doing nothing.
 */

// phpcs:ignore
class MongodbDummy {

  /**
   * A fake connection.
   *
   * @var \Mongo\Mongo
   */
  public $connection;

  /**
   * Pretend to return a collection.
   *
   * @return \MongodbDummy
   *   The returned value is actually a new MongodbDummy instance.
   */
  public function selectCollection() {
    return new MongodbDummy();
  }

  /**
   * Magic __call accepting any method name and doing nothing.
   *
   * @param string $name
   *   Ignored.
   * @param array $arguments
   *   All arguments are ignored.
   */
  public function __call($name, array $arguments) {
  }

}

/**
 * Returns the name to use for the collection.
 *
 * Also works with prefixes and simpletest.
 *
 * @param string $name
 *   The base name for the collection.
 *
 * @return string
 *   Unlike the base name, the returned name works with prefixes and simpletest.
 */
function mongodb_collection_name($name) {
  static $simpletest_prefix;

  // We call this function earlier than the database is initialized so we would
  // read the parent collection without this.
  if (!isset($simpletest_prefix)) {
    if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\\d+);/", $_SERVER['HTTP_USER_AGENT'], $matches)) {
      $simpletest_prefix = $matches[1];
    }
    else {
      $simpletest_prefix = '';
    }
  }

  // However, once the test information is initialized, simpletest_prefix
  // is no longer needed.
  if (!empty($GLOBALS['drupal_test_info']['test_run_id'])) {
    $simpletest_prefix = $GLOBALS['drupal_test_info']['test_run_id'];
  }
  return $simpletest_prefix . $name;
}

/**
 * Implements hook_test_group_finished().
 *
 * Testing helper: cleanup after test group.
 *
 * @throws \InvalidArgumentException
 *   If the database cannot be selected.
 * @throws \MongoConnectionException
 *   If the connection cannot be established.
 */
function mongodb_test_group_finished() {
  $aliases = variable_get('mongodb_connections', array());
  $aliases['default'] = TRUE;
  foreach (array_keys($aliases) as $alias) {
    $db = mongodb($alias);
    foreach ($db
      ->listCollections() as $collection) {
      if (preg_match('/\\.simpletest\\d+/', $collection)) {
        $db
          ->dropCollection($collection);
      }
    }
  }
}

/**
 * Allow for the database connection we are using to be changed.
 *
 * @param string $alias
 *   The alias that we want to change the connection for.
 * @param string $connection_name
 *   The name of the connection we will use.
 */
function mongodb_set_active_connection($alias, $connection_name = 'default') {

  // No need to check if the connection is valid as mongodb() does this.
  $alias_exists = isset($GLOBALS['conf']['mongodb_collections'][$alias]) && is_array($GLOBALS['conf']['mongodb_collections'][$alias]);
  if ($alias_exists & !empty($GLOBALS['conf']['mongodb_collections'][$alias]['db_connection'])) {
    $GLOBALS['conf']['mongodb_collections'][$alias]['db_connection'] = $connection_name;
  }
  else {
    $GLOBALS['conf']['mongodb_collections'][$alias] = $connection_name;
  }
}

/**
 * Return the next id in a sequence.
 *
 * @param string $name
 *   The name of the sequence.
 * @param int $existing_id
 *   The minimal value to be generated.
 *
 * @return int
 *   The id.
 *
 * @throws \MongoConnectionException
 */
function mongodb_next_id($name, $existing_id = 0) {

  // Atomically get the next id in the sequence.
  $mongo = mongodb();
  $cmd = array(
    'findandmodify' => mongodb_collection_name('sequence'),
    'query' => array(
      '_id' => $name,
    ),
    'update' => array(
      '$inc' => array(
        'value' => 1,
      ),
    ),
    'new' => TRUE,
  );

  // It's very likely that this is not necessary as command returns an array
  // not an exception. The increment will, however, will fix the problem of
  // the sequence not existing. Still, better safe than sorry.
  try {
    $sequence = $mongo
      ->command($cmd);
    $value = isset($sequence['value']['value']) ? $sequence['value']['value'] : 0;
  } catch (Exception $e) {
  }
  if (0 < $existing_id - $value + 1) {
    $cmd = array(
      'findandmodify' => mongodb_collection_name('sequence'),
      'query' => array(
        '_id' => $name,
      ),
      'update' => array(
        '$inc' => array(
          'value' => $existing_id - $value + 1,
        ),
      ),
      'upsert' => TRUE,
      'new' => TRUE,
    );
    $sequence = $mongo
      ->command($cmd);
    $value = isset($sequence['value']['value']) ? $sequence['value']['value'] : 0;
  }
  return $value;
}

/**
 * Returns default options for MongoDB write operations.
 *
 * @param bool $safe
 *   Set it to FALSE for "fire and forget" write operation.
 *
 * @return array
 *   Default options for Mongo write operations.
 */
function mongodb_default_write_options($safe = TRUE) {
  if ($safe) {
    if (version_compare(phpversion('mongo'), '1.5.0') == -1) {
      return array(
        'safe' => TRUE,
      );
    }
    else {
      return variable_get('mongodb_write_safe_options', array(
        'w' => 1,
      ));
    }
  }
  else {
    if (version_compare(phpversion('mongo'), '1.3.0') == -1) {
      return array();
    }
    else {
      return variable_get('mongodb_write_nonsafe_options', array(
        'w' => 0,
      ));
    }
  }
}

Functions

Namesort descending Description
mongodb Returns an MongoDB object.
mongodb_collection
mongodb_collection_name Returns the name to use for the collection.
mongodb_default_write_options Returns default options for MongoDB write operations.
mongodb_help Implements hook_help().
mongodb_next_id Return the next id in a sequence.
mongodb_set_active_connection Allow for the database connection we are using to be changed.
mongodb_test_group_finished Implements hook_test_group_finished().

Classes