Commit | Line | Data |
---|---|---|
2e638e3d SH |
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 | * | |
6fec1820 | 22 | * @package cachestore_mongodb |
2e638e3d SH |
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 | /** | |
30 | * The MongoDB Cache store. | |
31 | * | |
32 | * This cache store uses the MongoDB Native Driver. | |
33 | * For installation instructions have a look at the following two links: | |
34 | * - {@link http://www.php.net/manual/en/mongo.installation.php} | |
35 | * - {@link http://www.mongodb.org/display/DOCS/PHP+Language+Center} | |
36 | * | |
37 | * @copyright 2012 Sam Hemelryk | |
38 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
39 | */ | |
2b274ad0 | 40 | class cachestore_mongodb extends cache_store implements cache_is_configurable { |
2e638e3d SH |
41 | |
42 | /** | |
43 | * The name of the store | |
44 | * @var string | |
45 | */ | |
46 | protected $name; | |
47 | ||
48 | /** | |
49 | * The server connection string. Comma separated values. | |
50 | * @var string | |
51 | */ | |
52 | protected $server = 'mongodb://127.0.0.1:27017'; | |
53 | ||
54 | /** | |
55 | * The database connection options | |
56 | * @var array | |
57 | */ | |
58 | protected $options = array(); | |
59 | ||
60 | /** | |
61 | * The name of the database to use. | |
62 | * @var string | |
63 | */ | |
64 | protected $databasename = 'mcache'; | |
65 | ||
66 | /** | |
67 | * The Connection object | |
68 | * @var Mongo | |
69 | */ | |
a037c943 | 70 | protected $connection = false; |
2e638e3d SH |
71 | |
72 | /** | |
73 | * The Database Object | |
74 | * @var MongoDB | |
75 | */ | |
76 | protected $database; | |
77 | ||
78 | /** | |
79 | * The Collection object | |
80 | * @var MongoCollection | |
81 | */ | |
82 | protected $collection; | |
83 | ||
84 | /** | |
85 | * Determines if and what safe setting is to be used. | |
86 | * @var bool|int | |
87 | */ | |
88 | protected $usesafe = false; | |
89 | ||
90 | /** | |
91 | * If set to true then multiple identifiers will be requested and used. | |
92 | * @var bool | |
93 | */ | |
94 | protected $extendedmode = false; | |
95 | ||
96 | /** | |
97 | * The definition has which is used in the construction of the collection. | |
98 | * @var string | |
99 | */ | |
100 | protected $definitionhash = null; | |
101 | ||
102 | /** | |
5dfa3031 SH |
103 | * Set to true once this store is ready to be initialised and used. |
104 | * @var bool | |
105 | */ | |
106 | protected $isready = false; | |
107 | ||
4907a8ed SH |
108 | /** |
109 | * Set to true if the Mongo extension is < version 1.3. | |
110 | * If this is the case we must use the legacy Mongo class instead of MongoClient. | |
111 | * Mongo is backwards compatible, although obviously deprecated. | |
112 | * @var bool | |
113 | */ | |
114 | protected $legacymongo = false; | |
115 | ||
5dfa3031 SH |
116 | /** |
117 | * Constructs a new instance of the Mongo store. | |
118 | * | |
119 | * Noting that this function is not an initialisation. It is used to prepare the store for use. | |
120 | * The store will be initialised when required and will be provided with a cache_definition at that time. | |
121 | * | |
2e638e3d SH |
122 | * @param string $name |
123 | * @param array $configuration | |
124 | */ | |
125 | public function __construct($name, array $configuration = array()) { | |
126 | $this->name = $name; | |
127 | ||
128 | if (array_key_exists('server', $configuration)) { | |
129 | $this->server = $configuration['server']; | |
130 | } | |
131 | ||
132 | if (array_key_exists('replicaset', $configuration)) { | |
133 | $this->options['replicaSet'] = (string)$configuration['replicaset']; | |
134 | } | |
135 | if (array_key_exists('username', $configuration) && !empty($configuration['username'])) { | |
136 | $this->options['username'] = (string)$configuration['username']; | |
137 | } | |
138 | if (array_key_exists('password', $configuration) && !empty($configuration['password'])) { | |
139 | $this->options['password'] = (string)$configuration['password']; | |
140 | } | |
141 | if (array_key_exists('database', $configuration)) { | |
142 | $this->databasename = (string)$configuration['database']; | |
143 | } | |
144 | if (array_key_exists('usesafe', $configuration)) { | |
145 | $this->usesafe = $configuration['usesafe']; | |
146 | } | |
147 | if (array_key_exists('extendedmode', $configuration)) { | |
148 | $this->extendedmode = $configuration['extendedmode']; | |
149 | } | |
150 | ||
4907a8ed SH |
151 | // Test if the MongoClient class exists, if not we need to switch to legacy classes. |
152 | $this->legacymongo = (!class_exists('MongoClient')); | |
153 | ||
154 | // MongoClient from Mongo 1.3 onwards. Mongo for earlier versions. | |
155 | $class = ($this->legacymongo) ? 'Mongo' : 'MongoClient'; | |
f4cec2ec | 156 | try { |
4907a8ed | 157 | $this->connection = new $class($this->server, $this->options); |
f4cec2ec | 158 | $this->isready = true; |
65b3edc4 SH |
159 | } catch (MongoConnectionException $e) { |
160 | // We only want to catch MongoConnectionExceptions here. | |
f4cec2ec | 161 | } |
2e638e3d SH |
162 | } |
163 | ||
164 | /** | |
165 | * Returns true if the requirements of this store have been met. | |
166 | * @return bool | |
167 | */ | |
168 | public static function are_requirements_met() { | |
7c6f2e29 | 169 | return class_exists('MongoClient') || class_exists('Mongo'); |
2e638e3d SH |
170 | } |
171 | ||
2e638e3d SH |
172 | /** |
173 | * Returns the supported features. | |
174 | * @param array $configuration | |
175 | * @return int | |
176 | */ | |
177 | public static function get_supported_features(array $configuration = array()) { | |
178 | $supports = self::SUPPORTS_DATA_GUARANTEE; | |
179 | if (array_key_exists('extendedmode', $configuration) && $configuration['extendedmode']) { | |
180 | $supports += self::SUPPORTS_MULTIPLE_IDENTIFIERS; | |
181 | } | |
182 | return $supports; | |
183 | } | |
184 | ||
185 | /** | |
186 | * Returns an int describing the supported modes. | |
187 | * @param array $configuration | |
188 | * @return int | |
189 | */ | |
190 | public static function get_supported_modes(array $configuration = array()) { | |
90a3a620 | 191 | return self::MODE_APPLICATION; |
2e638e3d SH |
192 | } |
193 | ||
194 | /** | |
195 | * Initialises the store instance for use. | |
196 | * | |
5dfa3031 | 197 | * Once this has been done the cache is all set to be used. |
2e638e3d SH |
198 | * |
199 | * @param cache_definition $definition | |
200 | * @throws coding_exception | |
201 | */ | |
202 | public function initialise(cache_definition $definition) { | |
203 | if ($this->is_initialised()) { | |
204 | throw new coding_exception('This mongodb instance has already been initialised.'); | |
205 | } | |
65b3edc4 | 206 | $this->database = $this->connection->selectDB($this->databasename); |
2e638e3d | 207 | $this->definitionhash = $definition->generate_definition_hash(); |
2e638e3d | 208 | $this->collection = $this->database->selectCollection($this->definitionhash); |
4907a8ed SH |
209 | |
210 | $options = array('name' => 'idx_key'); | |
211 | if ($this->legacymongo) { | |
212 | $options['safe'] = $this->usesafe; | |
213 | } else { | |
214 | $options['w'] = $this->usesafe ? 1 : 0; | |
215 | } | |
216 | $this->collection->ensureIndex(array('key' => 1), $options); | |
2e638e3d SH |
217 | } |
218 | ||
219 | /** | |
220 | * Returns true if this store instance has been initialised. | |
221 | * @return bool | |
222 | */ | |
223 | public function is_initialised() { | |
224 | return ($this->database instanceof MongoDB); | |
225 | } | |
226 | ||
227 | /** | |
228 | * Returns true if this store instance is ready to use. | |
229 | * @return bool | |
230 | */ | |
231 | public function is_ready() { | |
232 | return $this->isready; | |
233 | } | |
234 | ||
235 | /** | |
236 | * Returns true if the given mode is supported by this store. | |
237 | * @param int $mode | |
238 | * @return bool | |
239 | */ | |
240 | public static function is_supported_mode($mode) { | |
241 | return ($mode == self::MODE_APPLICATION || $mode == self::MODE_SESSION); | |
242 | } | |
243 | ||
2e638e3d SH |
244 | /** |
245 | * Returns true if this store is making use of multiple identifiers. | |
246 | * @return bool | |
247 | */ | |
758dbdf8 | 248 | public function supports_multiple_identifiers() { |
2e638e3d SH |
249 | return $this->extendedmode; |
250 | } | |
251 | ||
2e638e3d SH |
252 | /** |
253 | * Retrieves an item from the cache store given its key. | |
254 | * | |
255 | * @param string $key The key to retrieve | |
256 | * @return mixed The data that was associated with the key, or false if the key did not exist. | |
257 | */ | |
258 | public function get($key) { | |
259 | if (!is_array($key)) { | |
260 | $key = array('key' => $key); | |
261 | } | |
170f821b | 262 | |
2e638e3d SH |
263 | $result = $this->collection->findOne($key); |
264 | if ($result === null || !array_key_exists('data', $result)) { | |
265 | return false; | |
266 | } | |
267 | $data = @unserialize($result['data']); | |
268 | return $data; | |
269 | } | |
270 | ||
271 | /** | |
272 | * Retrieves several items from the cache store in a single transaction. | |
273 | * | |
274 | * 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. | |
275 | * | |
276 | * @param array $keys The array of keys to retrieve | |
277 | * @return array An array of items from the cache. | |
278 | */ | |
279 | public function get_many($keys) { | |
280 | if ($this->extendedmode) { | |
281 | $query = $this->get_many_extendedmode_query($keys); | |
282 | $keyarray = array(); | |
283 | foreach ($keys as $key) { | |
284 | $keyarray[] = $key['key']; | |
285 | } | |
286 | $keys = $keyarray; | |
287 | $query = array('key' => array('$in' => $keys)); | |
288 | } else { | |
289 | $query = array('key' => array('$in' => $keys)); | |
290 | } | |
291 | $cursor = $this->collection->find($query); | |
292 | $results = array(); | |
293 | foreach ($cursor as $result) { | |
32c981e6 | 294 | $id = (string)$result['key']; |
2e638e3d SH |
295 | $results[$id] = unserialize($result['data']); |
296 | } | |
297 | foreach ($keys as $key) { | |
298 | if (!array_key_exists($key, $results)) { | |
299 | $results[$key] = false; | |
300 | } | |
301 | } | |
302 | return $results; | |
303 | } | |
304 | ||
305 | /** | |
306 | * Sets an item in the cache given its key and data value. | |
307 | * | |
308 | * @param string $key The key to use. | |
309 | * @param mixed $data The data to set. | |
310 | * @return bool True if the operation was a success false otherwise. | |
311 | */ | |
312 | public function set($key, $data) { | |
313 | if (!is_array($key)) { | |
314 | $record = array( | |
315 | 'key' => $key | |
316 | ); | |
317 | } else { | |
318 | $record = $key; | |
319 | } | |
320 | $record['data'] = serialize($data); | |
4907a8ed SH |
321 | $options = array('upsert' => true); |
322 | if ($this->legacymongo) { | |
323 | $options['safe'] = $this->usesafe; | |
324 | } else { | |
325 | $options['w'] = $this->usesafe ? 1 : 0; | |
326 | } | |
2e638e3d SH |
327 | $this->delete($key); |
328 | $result = $this->collection->insert($record, $options); | |
32c981e6 SH |
329 | if ($result === true) { |
330 | // Safe mode is off. | |
331 | return true; | |
332 | } else if (is_array($result)) { | |
333 | if (empty($result['ok']) || isset($result['err'])) { | |
334 | return false; | |
335 | } | |
336 | return true; | |
337 | } | |
338 | // Who knows? | |
339 | return false; | |
2e638e3d SH |
340 | } |
341 | ||
342 | /** | |
343 | * Sets many items in the cache in a single transaction. | |
344 | * | |
345 | * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two | |
346 | * keys, 'key' and 'value'. | |
347 | * @return int The number of items successfully set. It is up to the developer to check this matches the number of items | |
348 | * sent ... if they care that is. | |
349 | */ | |
350 | public function set_many(array $keyvaluearray) { | |
351 | $count = 0; | |
352 | foreach ($keyvaluearray as $pair) { | |
353 | $result = $this->set($pair['key'], $pair['value']); | |
32c981e6 | 354 | if ($result === true) { |
2e638e3d SH |
355 | $count++; |
356 | } | |
357 | } | |
32c981e6 | 358 | return $count; |
2e638e3d SH |
359 | } |
360 | ||
361 | /** | |
362 | * Deletes an item from the cache store. | |
363 | * | |
364 | * @param string $key The key to delete. | |
365 | * @return bool Returns true if the operation was a success, false otherwise. | |
366 | */ | |
367 | public function delete($key) { | |
368 | if (!is_array($key)) { | |
369 | $criteria = array( | |
370 | 'key' => $key | |
371 | ); | |
372 | } else { | |
373 | $criteria = $key; | |
374 | } | |
4907a8ed SH |
375 | $options = array('justOne' => false); |
376 | if ($this->legacymongo) { | |
377 | $options['safe'] = $this->usesafe; | |
378 | } else { | |
379 | $options['w'] = $this->usesafe ? 1 : 0; | |
380 | } | |
2e638e3d | 381 | $result = $this->collection->remove($criteria, $options); |
32c981e6 SH |
382 | |
383 | if ($result === true) { | |
384 | // Safe mode. | |
385 | return true; | |
386 | } else if (is_array($result)) { | |
387 | if (empty($result['ok']) || isset($result['err'])) { | |
388 | return false; | |
389 | } else if (empty($result['n'])) { | |
390 | // Nothing was removed. | |
391 | return false; | |
392 | } | |
393 | return true; | |
2e638e3d | 394 | } |
32c981e6 SH |
395 | // Who knows? |
396 | return false; | |
2e638e3d SH |
397 | } |
398 | ||
399 | /** | |
400 | * Deletes several keys from the cache in a single action. | |
401 | * | |
402 | * @param array $keys The keys to delete | |
403 | * @return int The number of items successfully deleted. | |
404 | */ | |
405 | public function delete_many(array $keys) { | |
406 | $count = 0; | |
407 | foreach ($keys as $key) { | |
408 | if ($this->delete($key)) { | |
409 | $count++; | |
410 | } | |
411 | } | |
412 | return $count; | |
413 | } | |
414 | ||
415 | /** | |
416 | * Purges the cache deleting all items within it. | |
417 | * | |
418 | * @return boolean True on success. False otherwise. | |
419 | */ | |
420 | public function purge() { | |
f4cec2ec MS |
421 | if ($this->isready) { |
422 | $this->collection->drop(); | |
423 | $this->collection = $this->database->selectCollection($this->definitionhash); | |
424 | } | |
425 | ||
426 | return true; | |
2e638e3d SH |
427 | } |
428 | ||
429 | /** | |
430 | * Takes the object from the add instance store and creates a configuration array that can be used to initialise an instance. | |
431 | * | |
432 | * @param stdClass $data | |
433 | * @return array | |
434 | */ | |
435 | public static function config_get_configuration_array($data) { | |
436 | $return = array( | |
437 | 'server' => $data->server, | |
438 | 'database' => $data->database, | |
439 | 'extendedmode' => (!empty($data->extendedmode)) | |
440 | ); | |
441 | if (!empty($data->username)) { | |
442 | $return['username'] = $data->username; | |
443 | } | |
444 | if (!empty($data->password)) { | |
445 | $return['password'] = $data->password; | |
446 | } | |
447 | if (!empty($data->replicaset)) { | |
448 | $return['replicaset'] = $data->replicaset; | |
449 | } | |
450 | if (!empty($data->usesafe)) { | |
451 | $return['usesafe'] = true; | |
452 | if (!empty($data->usesafevalue)) { | |
453 | $return['usesafe'] = (int)$data->usesafevalue; | |
d837df0d | 454 | $return['usesafevalue'] = $return['usesafe']; |
2e638e3d SH |
455 | } |
456 | } | |
457 | return $return; | |
458 | } | |
459 | ||
81ede547 SH |
460 | /** |
461 | * Allows the cache store to set its data against the edit form before it is shown to the user. | |
462 | * | |
463 | * @param moodleform $editform | |
464 | * @param array $config | |
465 | */ | |
466 | public static function config_set_edit_form_data(moodleform $editform, array $config) { | |
467 | $data = array(); | |
468 | if (!empty($config['server'])) { | |
469 | $data['server'] = $config['server']; | |
470 | } | |
471 | if (!empty($config['database'])) { | |
472 | $data['database'] = $config['database']; | |
473 | } | |
d837df0d SH |
474 | if (isset($config['extendedmode'])) { |
475 | $data['extendedmode'] = (bool)$config['extendedmode']; | |
81ede547 SH |
476 | } |
477 | if (!empty($config['username'])) { | |
478 | $data['username'] = $config['username']; | |
479 | } | |
480 | if (!empty($config['password'])) { | |
481 | $data['password'] = $config['password']; | |
482 | } | |
483 | if (!empty($config['replicaset'])) { | |
484 | $data['replicaset'] = $config['replicaset']; | |
485 | } | |
d837df0d SH |
486 | if (isset($config['usesafevalue'])) { |
487 | $data['usesafe'] = true; | |
488 | $data['usesafevalue'] = (int)$data['usesafe']; | |
489 | } else if (isset($config['usesafe'])) { | |
490 | $data['usesafe'] = (bool)$config['usesafe']; | |
81ede547 SH |
491 | } |
492 | $editform->set_data($data); | |
493 | } | |
494 | ||
2e638e3d SH |
495 | /** |
496 | * Performs any necessary clean up when the store instance is being deleted. | |
497 | */ | |
59ca73ff | 498 | public function instance_deleted() { |
a037c943 SH |
499 | // We can't use purge here that acts upon a collection. |
500 | // Instead we must drop the named database. | |
501 | if ($this->connection) { | |
502 | $connection = $this->connection; | |
503 | } else { | |
bb4c3916 | 504 | try { |
4907a8ed SH |
505 | // MongoClient from Mongo 1.3 onwards. Mongo for earlier versions. |
506 | $class = ($this->legacymongo) ? 'Mongo' : 'MongoClient'; | |
507 | $connection = new $class($this->server, $this->options); | |
bb4c3916 SH |
508 | } catch (MongoConnectionException $e) { |
509 | // We only want to catch MongoConnectionExceptions here. | |
510 | // If the server cannot be connected to we cannot clean it. | |
511 | return; | |
512 | } | |
a037c943 SH |
513 | } |
514 | $database = $connection->selectDB($this->databasename); | |
515 | $database->drop(); | |
516 | $connection = null; | |
517 | $database = null; | |
518 | // Explicitly unset things to cause a close. | |
519 | $this->collection = null; | |
520 | $this->database = null; | |
521 | $this->connection = null; | |
59ca73ff MS |
522 | } |
523 | ||
2e638e3d SH |
524 | /** |
525 | * Generates an instance of the cache store that can be used for testing. | |
526 | * | |
527 | * @param cache_definition $definition | |
528 | * @return false | |
529 | */ | |
530 | public static function initialise_test_instance(cache_definition $definition) { | |
531 | if (!self::are_requirements_met()) { | |
532 | return false; | |
533 | } | |
534 | ||
6fec1820 | 535 | $config = get_config('cachestore_mongodb'); |
2e638e3d SH |
536 | if (empty($config->testserver)) { |
537 | return false; | |
538 | } | |
2e638e3d SH |
539 | $configuration = array(); |
540 | $configuration['server'] = $config->testserver; | |
541 | if (!empty($config->testreplicaset)) { | |
542 | $configuration['replicaset'] = $config->testreplicaset; | |
543 | } | |
544 | if (!empty($config->testusername)) { | |
545 | $configuration['username'] = $config->testusername; | |
546 | } | |
547 | if (!empty($config->testpassword)) { | |
548 | $configuration['password'] = $config->testpassword; | |
549 | } | |
550 | if (!empty($config->testdatabase)) { | |
551 | $configuration['database'] = $config->testdatabase; | |
552 | } | |
32c981e6 | 553 | $configuration['usesafe'] = 1; |
2e638e3d SH |
554 | if (!empty($config->testextendedmode)) { |
555 | $configuration['extendedmode'] = (bool)$config->testextendedmode; | |
556 | } | |
557 | ||
6fec1820 | 558 | $store = new cachestore_mongodb('Test mongodb', $configuration); |
2e638e3d SH |
559 | $store->initialise($definition); |
560 | ||
561 | return $store; | |
562 | } | |
34c84c72 SH |
563 | |
564 | /** | |
565 | * Returns the name of this instance. | |
566 | * @return string | |
567 | */ | |
568 | public function my_name() { | |
569 | return $this->name; | |
570 | } | |
a3f3ea26 | 571 | } |