Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * The library file for the MongoDB store plugin.
  19   *
  20   * This file is part of the MongoDB store plugin, it contains the API for interacting with an instance of the store.
  21   *
  22   * @package    cachestore_mongodb
  23   * @copyright  2012 Sam Hemelryk
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  require_once ('MongoDB/functions.php');
  30  
  31  /**
  32   * The MongoDB Cache store.
  33   *
  34   * This cache store uses the MongoDB Native Driver and the MongoDB PHP Library.
  35   * For installation instructions have a look at the following two links:
  36   *  - {@link http://php.net/manual/en/set.mongodb.php}
  37   *  - {@link https://docs.mongodb.com/ecosystem/drivers/php/}
  38   *
  39   * @copyright  2012 Sam Hemelryk
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class cachestore_mongodb extends cache_store implements cache_is_configurable {
  43  
  44      /**
  45       * The name of the store
  46       * @var string
  47       */
  48      protected $name;
  49  
  50      /**
  51       * The server connection string. Comma separated values.
  52       * @var string
  53       */
  54      protected $server = 'mongodb://127.0.0.1:27017';
  55  
  56      /**
  57       * The database connection options
  58       * @var array
  59       */
  60      protected $options = array();
  61  
  62      /**
  63       * The name of the database to use.
  64       * @var string
  65       */
  66      protected $databasename = 'mcache';
  67  
  68      /**
  69       * The Connection object
  70       * @var MongoDB/Client
  71       */
  72      protected $connection = false;
  73  
  74      /**
  75       * The Database Object
  76       * @var MongoDB/Database
  77       */
  78      protected $database;
  79  
  80      /**
  81       * The Collection object
  82       * @var MongoDB/Collection
  83       */
  84      protected $collection;
  85  
  86      /**
  87       * Determines if and what safe setting is to be used.
  88       * @var bool|int
  89       */
  90      protected $usesafe = true;
  91  
  92      /**
  93       * If set to true then multiple identifiers will be requested and used.
  94       * @var bool
  95       */
  96      protected $extendedmode = false;
  97  
  98      /**
  99       * The definition has which is used in the construction of the collection.
 100       * @var string
 101       */
 102      protected $definitionhash = null;
 103  
 104      /**
 105       * Set to true once this store is ready to be initialised and used.
 106       * @var bool
 107       */
 108      protected $isready = false;
 109  
 110      /**
 111       * Constructs a new instance of the Mongo store.
 112       *
 113       * Noting that this function is not an initialisation. It is used to prepare the store for use.
 114       * The store will be initialised when required and will be provided with a cache_definition at that time.
 115       *
 116       * @param string $name
 117       * @param array $configuration
 118       */
 119      public function __construct($name, array $configuration = array()) {
 120          $this->name = $name;
 121  
 122          if (array_key_exists('server', $configuration)) {
 123              $this->server = $configuration['server'];
 124          }
 125  
 126          if (array_key_exists('replicaset', $configuration)) {
 127              $this->options['replicaSet'] = (string)$configuration['replicaset'];
 128          }
 129          if (array_key_exists('username', $configuration) && !empty($configuration['username'])) {
 130              $this->options['username'] = (string)$configuration['username'];
 131          }
 132          if (array_key_exists('password', $configuration) && !empty($configuration['password'])) {
 133              $this->options['password'] = (string)$configuration['password'];
 134          }
 135          if (array_key_exists('database', $configuration)) {
 136              $this->databasename = (string)$configuration['database'];
 137          }
 138          if (array_key_exists('usesafe', $configuration)) {
 139              $this->usesafe = $configuration['usesafe'];
 140          }
 141          if (array_key_exists('extendedmode', $configuration)) {
 142              $this->extendedmode = $configuration['extendedmode'];
 143          }
 144  
 145          try {
 146              $this->connection = new MongoDB\Client($this->server, $this->options);
 147              // Required because MongoDB\Client does not try to connect to the server
 148              $rp = new MongoDB\Driver\ReadPreference(MongoDB\Driver\ReadPreference::RP_PRIMARY);
 149              $this->connection->getManager()->selectServer($rp);
 150              $this->isready = true;
 151          } catch (MongoDB\Driver\Exception\RuntimeException $e) {
 152              // We only want to catch RuntimeException here.
 153          }
 154      }
 155  
 156      /**
 157       * Returns true if the requirements of this store have been met.
 158       * @return bool
 159       */
 160      public static function are_requirements_met() {
 161          return version_compare(phpversion('mongodb'), '1.14', 'ge');
 162      }
 163  
 164      /**
 165       * Returns the supported features.
 166       * @param array $configuration
 167       * @return int
 168       */
 169      public static function get_supported_features(array $configuration = array()) {
 170          $supports = self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS;
 171          if (array_key_exists('extendedmode', $configuration) && $configuration['extendedmode']) {
 172              $supports += self::SUPPORTS_MULTIPLE_IDENTIFIERS;
 173          }
 174          return $supports;
 175      }
 176  
 177      /**
 178       * Returns an int describing the supported modes.
 179       * @param array $configuration
 180       * @return int
 181       */
 182      public static function get_supported_modes(array $configuration = array()) {
 183          return self::MODE_APPLICATION;
 184      }
 185  
 186      /**
 187       * Initialises the store instance for use.
 188       *
 189       * Once this has been done the cache is all set to be used.
 190       *
 191       * @param cache_definition $definition
 192       * @throws coding_exception
 193       */
 194      public function initialise(cache_definition $definition) {
 195          if ($this->is_initialised()) {
 196              throw new coding_exception('This mongodb instance has already been initialised.');
 197          }
 198          $this->database = $this->connection->selectDatabase($this->databasename);
 199          $this->definitionhash = 'm'.$definition->generate_definition_hash();
 200          $this->collection = $this->database->selectCollection($this->definitionhash);
 201  
 202          $options = array('name' => 'idx_key');
 203  
 204          $w = $this->usesafe ? 1 : 0;
 205          $wc = new MongoDB\Driver\WriteConcern($w);
 206  
 207          $options['writeConcern'] = $wc;
 208  
 209          $this->collection->createIndex(array('key' => 1), $options);
 210      }
 211  
 212      /**
 213       * Returns true if this store instance has been initialised.
 214       * @return bool
 215       */
 216      public function is_initialised() {
 217          return ($this->database instanceof MongoDB\Database);
 218      }
 219  
 220      /**
 221       * Returns true if this store instance is ready to use.
 222       * @return bool
 223       */
 224      public function is_ready() {
 225          return $this->isready;
 226      }
 227  
 228      /**
 229       * Returns true if the given mode is supported by this store.
 230       * @param int $mode
 231       * @return bool
 232       */
 233      public static function is_supported_mode($mode) {
 234          return ($mode == self::MODE_APPLICATION || $mode == self::MODE_SESSION);
 235      }
 236  
 237      /**
 238       * Returns true if this store is making use of multiple identifiers.
 239       * @return bool
 240       */
 241      public function supports_multiple_identifiers() {
 242          return $this->extendedmode;
 243      }
 244  
 245      /**
 246       * Retrieves an item from the cache store given its key.
 247       *
 248       * @param string $key The key to retrieve
 249       * @return mixed The data that was associated with the key, or false if the key did not exist.
 250       */
 251      public function get($key) {
 252          if (!is_array($key)) {
 253              $key = array('key' => $key);
 254          }
 255  
 256          $result = $this->collection->findOne($key);
 257          // Note $result is really an object, BSONDocument extending ArrayObject,
 258          // which implements ArrayAccess. That enables access to its information
 259          // using square brackets and some array operations. But, it seems that
 260          // it's not enough for array_key_exists() to operate on it. Hence, we
 261          // are explicitly casting to array, after having checked that the operation
 262          // doesn't incur into any performance penalty.
 263          if ($result === null || !array_key_exists('data', (array)$result)) {
 264              return false;
 265          }
 266          $data = @unserialize($result['data']);
 267          return $data;
 268      }
 269  
 270      /**
 271       * Retrieves several items from the cache store in a single transaction.
 272       *
 273       * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
 274       *
 275       * @param array $keys The array of keys to retrieve
 276       * @return array An array of items from the cache.
 277       */
 278      public function get_many($keys) {
 279          if ($this->extendedmode) {
 280              $query = $this->get_many_extendedmode_query($keys);
 281              $keyarray = array();
 282              foreach ($keys as $key) {
 283                  $keyarray[] = $key['key'];
 284              }
 285              $keys = $keyarray;
 286              $query = array('key' => array('$in' => $keys));
 287          } else {
 288              $query = array('key' => array('$in' => $keys));
 289          }
 290          $cursor = $this->collection->find($query);
 291          $results = array();
 292          foreach ($cursor as $result) {
 293              $id = (string)$result['key'];
 294              $results[$id] = unserialize($result['data']);
 295          }
 296          foreach ($keys as $key) {
 297              if (!array_key_exists($key, $results)) {
 298                  $results[$key] = false;
 299              }
 300          }
 301          return $results;
 302      }
 303  
 304      /**
 305       * Sets an item in the cache given its key and data value.
 306       *
 307       * @param string $key The key to use.
 308       * @param mixed $data The data to set.
 309       * @return bool True if the operation was a success false otherwise.
 310       */
 311      public function set($key, $data) {
 312          if (!is_array($key)) {
 313              $record = array(
 314                  'key' => $key
 315              );
 316          } else {
 317              $record = $key;
 318          }
 319          $record['data'] = serialize($data);
 320          $options = array('upsert' => true);
 321  
 322          $w = $this->usesafe ? 1 : 0;
 323          $wc = new MongoDB\Driver\WriteConcern($w);
 324  
 325          $options['writeConcern'] = $wc;
 326  
 327          $this->delete($key);
 328          try {
 329              $this->collection->insertOne($record, $options);
 330          } catch (MongoDB\Exception\Exception $e) {
 331              return false;
 332          }
 333  
 334          return true;
 335      }
 336  
 337      /**
 338       * Sets many items in the cache in a single transaction.
 339       *
 340       * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
 341       *      keys, 'key' and 'value'.
 342       * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
 343       *      sent ... if they care that is.
 344       */
 345      public function set_many(array $keyvaluearray) {
 346          $count = 0;
 347          foreach ($keyvaluearray as $pair) {
 348              $result = $this->set($pair['key'], $pair['value']);
 349              if ($result === true) {
 350                   $count++;
 351              }
 352          }
 353          return $count;
 354      }
 355  
 356      /**
 357       * Deletes an item from the cache store.
 358       *
 359       * @param string $key The key to delete.
 360       * @return bool Returns true if the operation was a success, false otherwise.
 361       */
 362      public function delete($key) {
 363          if (!is_array($key)) {
 364              $criteria = array(
 365                  'key' => $key
 366              );
 367          } else {
 368              $criteria = $key;
 369          }
 370          $options = array('justOne' => false);
 371  
 372          $w = $this->usesafe ? 1 : 0;
 373          $wc = new MongoDB\Driver\WriteConcern($w);
 374  
 375          $options['writeConcern'] = $wc;
 376  
 377          try {
 378              $result = $this->collection->deleteOne($criteria, $options);
 379          } catch (\MongoDB\Exception $e) {
 380              return false;
 381          }
 382  
 383          if (empty($result->getDeletedCount())) {
 384              return false;
 385          }
 386  
 387          return true;
 388      }
 389  
 390      /**
 391       * Deletes several keys from the cache in a single action.
 392       *
 393       * @param array $keys The keys to delete
 394       * @return int The number of items successfully deleted.
 395       */
 396      public function delete_many(array $keys) {
 397          $count = 0;
 398          foreach ($keys as $key) {
 399              if ($this->delete($key)) {
 400                  $count++;
 401              }
 402          }
 403          return $count;
 404      }
 405  
 406      /**
 407       * Purges the cache deleting all items within it.
 408       *
 409       * @return boolean True on success. False otherwise.
 410       */
 411      public function purge() {
 412          if ($this->isready) {
 413              $this->collection->drop();
 414              $this->collection = $this->database->selectCollection($this->definitionhash);
 415          }
 416  
 417          return true;
 418      }
 419  
 420      /**
 421       * Takes the object from the add instance store and creates a configuration array that can be used to initialise an instance.
 422       *
 423       * @param stdClass $data
 424       * @return array
 425       */
 426      public static function config_get_configuration_array($data) {
 427          $return = array(
 428              'server' => $data->server,
 429              'database' => $data->database,
 430              'extendedmode' => (!empty($data->extendedmode))
 431          );
 432          if (!empty($data->username)) {
 433              $return['username'] = $data->username;
 434          }
 435          if (!empty($data->password)) {
 436              $return['password'] = $data->password;
 437          }
 438          if (!empty($data->replicaset)) {
 439              $return['replicaset'] = $data->replicaset;
 440          }
 441          if (!empty($data->usesafe)) {
 442              $return['usesafe'] = true;
 443              if (!empty($data->usesafevalue)) {
 444                  $return['usesafe'] = (int)$data->usesafevalue;
 445                  $return['usesafevalue'] = $return['usesafe'];
 446              }
 447          }
 448          return $return;
 449      }
 450  
 451      /**
 452       * Allows the cache store to set its data against the edit form before it is shown to the user.
 453       *
 454       * @param moodleform $editform
 455       * @param array $config
 456       */
 457      public static function config_set_edit_form_data(moodleform $editform, array $config) {
 458          $data = array();
 459          if (!empty($config['server'])) {
 460              $data['server'] = $config['server'];
 461          }
 462          if (!empty($config['database'])) {
 463              $data['database'] = $config['database'];
 464          }
 465          if (isset($config['extendedmode'])) {
 466              $data['extendedmode'] = (bool)$config['extendedmode'];
 467          }
 468          if (!empty($config['username'])) {
 469              $data['username'] = $config['username'];
 470          }
 471          if (!empty($config['password'])) {
 472              $data['password'] = $config['password'];
 473          }
 474          if (!empty($config['replicaset'])) {
 475              $data['replicaset'] = $config['replicaset'];
 476          }
 477          if (isset($config['usesafevalue'])) {
 478              $data['usesafe'] = true;
 479              $data['usesafevalue'] = (int)$data['usesafe'];
 480          } else if (isset($config['usesafe'])) {
 481              $data['usesafe'] = (bool)$config['usesafe'];
 482          }
 483          $editform->set_data($data);
 484      }
 485  
 486      /**
 487       * Performs any necessary clean up when the store instance is being deleted.
 488       */
 489      public function instance_deleted() {
 490          // We can't use purge here that acts upon a collection.
 491          // Instead we must drop the named database.
 492          if (!$this->is_ready()) {
 493              return;
 494          }
 495          $database = $this->connection->selectDatabase($this->databasename);
 496          $database->drop();
 497          $connection = null;
 498          $database = null;
 499          // Explicitly unset things to cause a close.
 500          $this->collection = null;
 501          $this->database = null;
 502          $this->connection = null;
 503      }
 504  
 505      /**
 506       * Generates an instance of the cache store that can be used for testing.
 507       *
 508       * @param cache_definition $definition
 509       * @return false
 510       */
 511      public static function initialise_test_instance(cache_definition $definition) {
 512          if (!self::are_requirements_met()) {
 513              return false;
 514          }
 515  
 516          $config = get_config('cachestore_mongodb');
 517          if (empty($config->testserver)) {
 518              return false;
 519          }
 520          $configuration = array();
 521          $configuration['server'] = $config->testserver;
 522          if (!empty($config->testreplicaset)) {
 523              $configuration['replicaset'] = $config->testreplicaset;
 524          }
 525          if (!empty($config->testusername)) {
 526              $configuration['username'] = $config->testusername;
 527          }
 528          if (!empty($config->testpassword)) {
 529              $configuration['password'] = $config->testpassword;
 530          }
 531          if (!empty($config->testdatabase)) {
 532              $configuration['database'] = $config->testdatabase;
 533          }
 534          $configuration['usesafe'] = 1;
 535          if (!empty($config->testextendedmode)) {
 536              $configuration['extendedmode'] = (bool)$config->testextendedmode;
 537          }
 538  
 539          $store = new cachestore_mongodb('Test mongodb', $configuration);
 540          if (!$store->is_ready()) {
 541              return false;
 542          }
 543          $store->initialise($definition);
 544  
 545          return $store;
 546      }
 547  
 548      /**
 549       * Generates an instance of the cache store that can be used for testing.
 550       *
 551       * @param cache_definition $definition
 552       * @return false
 553       */
 554      public static function unit_test_configuration() {
 555          $configuration = array();
 556          $configuration['usesafe'] = 1;
 557  
 558          // If the configuration is not defined correctly, return only the configuration know about.
 559          if (defined('TEST_CACHESTORE_MONGODB_TESTSERVER')) {
 560              $configuration['server'] = TEST_CACHESTORE_MONGODB_TESTSERVER;
 561          }
 562  
 563          return $configuration;
 564      }
 565  
 566      /**
 567       * Returns the name of this instance.
 568       * @return string
 569       */
 570      public function my_name() {
 571          return $this->name;
 572      }
 573  
 574      /**
 575       * Returns true if this cache store instance is both suitable for testing, and ready for testing.
 576       *
 577       * Cache stores that support being used as the default store for unit and acceptance testing should
 578       * override this function and return true if there requirements have been met.
 579       *
 580       * @return bool
 581       */
 582      public static function ready_to_be_used_for_testing() {
 583          return defined('TEST_CACHESTORE_MONGODB_TESTSERVER');
 584      }
 585  }