You are here

mongodb.module in MongoDB 6

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

A library of common mechanisms for modules using MongoDB.

File

mongodb.module
View source
<?php

/**
 * @file
 * A library of common mechanisms for modules using 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
 *   An optional alias for the connection.
 */
function mongodb($alias = 'default') {
  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'];
  $options = $connection['connection_options'] + array(
    'connect' => TRUE,
  );
  $db = $connection['db'];
  if (!isset($mongo_objects[$host][$db])) {
    try {

      // Use the 1.3 client if available.
      if (class_exists('MongoClient')) {
        $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) {
      $mongo_objects[$host][$db] = new MongoDummy();
      throw $e;
    }
  }
  return $mongo_objects[$host][$db];
}

/**
 * Returns a MongoCollection object.
 */
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 comptability.
    $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 wraps a MongoCollection with debug() instructions.
 */
class MongoDebugCollection {

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

  /**
   * Performs a find() on the original cursor, wrapping it in debug() calls.
   *
   * @param array $query
   *   The query criteria, as in the original MongoCollection::find().
   * @param array $fields
   *   The query result fields, as in the original MongoCollection::find().
   *
   * @return \MongoDebugCursor
   *   A new instance of a wrapped cursor.
   */
  public function find($query = array(), $fields = array()) {
    debug('find');
    debug($query);
    debug($fields);
    return new MongoDebugCursor($this->collection
      ->find($query, $fields));
  }

  /**
   * {@inheritdoc}
   */
  public function __call($name, $arguments) {
    debug($name);
    debug($arguments);
    return call_user_func_array(array(
      $this->collection,
      $name,
    ), $arguments);
  }

}

/**
 * Class MongoDebugCursor wraps a cursor with debug() instructions.
 */
class MongoDebugCursor {

  /**
   * The cursor wrapped by this instance.
   *
   * @var \MongoCursor
   */
  protected $cursor;

  /**
   * Constructor.
   *
   * @param MongoCursor $cursor
   *   The cursor to wrap.
   */
  public function __construct(MongoCursor $cursor) {
    $this->cursor = $cursor;
  }

  /**
   * {@inheritdoc}
   */
  public function __call($name, $arguments) {
    debug($name);
    debug($arguments);
    return call_user_func_array(array(
      $this->cursor,
      $name,
    ), $arguments);
  }

}

/**
 * Class MongoDummy is a mock class usable as a database or collection.
 *
 * It accepts any method and ignores it.
 */
class MongoDummy {
  public $connection;

  /**
   * Pretend to select a collection.
   *
   * @return \MongoDummy
   *   A fake collection object.
   */
  public function selectCollection() {
    return new MongoDummy();
  }

  /**
   * Accepts any arguments and ignores them.
   *
   * @param string $name
   *   The name of the method being called on the instance.
   * @param mixed[] $arguments
   *   The arguments passed to the method.
   */
  public function __call($name, array $arguments) {
  }

}

/**
 * Returns the name to use for the collection.
 *
 * 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) {
  global $db_prefix;
  static $simpletest_prefix;

  // We call this function earlier than the database is initalized 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;
}

/**
 * Testing helper: cleanup after test group.
 */
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 next id.
 */
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 Returns a MongoCollection object.
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 Testing helper: cleanup after test group.

Classes

Namesort descending Description
MongoDebugCollection Class MongoDebugCollection wraps a MongoCollection with debug() instructions.
MongoDebugCursor Class MongoDebugCursor wraps a cursor with debug() instructions.
MongoDummy Class MongoDummy is a mock class usable as a database or collection.