See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body