MDL-36768 cache: replaced cache_store interface with abstract class
[moodle.git] / cache / stores / mongodb / lib.php
CommitLineData
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
27defined('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 */
75cde6b9 40class cachestore_mongodb extends cache_store {
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 */
70 protected $connection;
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 /**
103 * Constructs a new instance of the Mongo store but does not connect to it.
104 * @param string $name
105 * @param array $configuration
106 */
107 public function __construct($name, array $configuration = array()) {
108 $this->name = $name;
109
110 if (array_key_exists('server', $configuration)) {
111 $this->server = $configuration['server'];
112 }
113
114 if (array_key_exists('replicaset', $configuration)) {
115 $this->options['replicaSet'] = (string)$configuration['replicaset'];
116 }
117 if (array_key_exists('username', $configuration) && !empty($configuration['username'])) {
118 $this->options['username'] = (string)$configuration['username'];
119 }
120 if (array_key_exists('password', $configuration) && !empty($configuration['password'])) {
121 $this->options['password'] = (string)$configuration['password'];
122 }
123 if (array_key_exists('database', $configuration)) {
124 $this->databasename = (string)$configuration['database'];
125 }
126 if (array_key_exists('usesafe', $configuration)) {
127 $this->usesafe = $configuration['usesafe'];
128 }
129 if (array_key_exists('extendedmode', $configuration)) {
130 $this->extendedmode = $configuration['extendedmode'];
131 }
132
133 $this->isready = self::are_requirements_met();
134 }
135
136 /**
137 * Returns true if the requirements of this store have been met.
138 * @return bool
139 */
140 public static function are_requirements_met() {
141 return class_exists('Mongo');
142 }
143
2e638e3d
SH
144 /**
145 * Returns the supported features.
146 * @param array $configuration
147 * @return int
148 */
149 public static function get_supported_features(array $configuration = array()) {
150 $supports = self::SUPPORTS_DATA_GUARANTEE;
151 if (array_key_exists('extendedmode', $configuration) && $configuration['extendedmode']) {
152 $supports += self::SUPPORTS_MULTIPLE_IDENTIFIERS;
153 }
154 return $supports;
155 }
156
157 /**
158 * Returns an int describing the supported modes.
159 * @param array $configuration
160 * @return int
161 */
162 public static function get_supported_modes(array $configuration = array()) {
163 return self::MODE_APPLICATION + self::MODE_SESSION;
164 }
165
166 /**
167 * Initialises the store instance for use.
168 *
169 * This function is reponsible for making the connection.
170 *
171 * @param cache_definition $definition
172 * @throws coding_exception
173 */
174 public function initialise(cache_definition $definition) {
175 if ($this->is_initialised()) {
176 throw new coding_exception('This mongodb instance has already been initialised.');
177 }
178 $this->definitionhash = $definition->generate_definition_hash();
179 $this->connection = new Mongo($this->server, $this->options);
180 $this->database = $this->connection->selectDB($this->databasename);
181 $this->collection = $this->database->selectCollection($this->definitionhash);
182 $this->collection->ensureIndex(array('key' => 1), array(
183 'safe' => $this->usesafe,
184 'name' => 'idx_key'
185 ));
186 }
187
188 /**
189 * Returns true if this store instance has been initialised.
190 * @return bool
191 */
192 public function is_initialised() {
193 return ($this->database instanceof MongoDB);
194 }
195
196 /**
197 * Returns true if this store instance is ready to use.
198 * @return bool
199 */
200 public function is_ready() {
201 return $this->isready;
202 }
203
204 /**
205 * Returns true if the given mode is supported by this store.
206 * @param int $mode
207 * @return bool
208 */
209 public static function is_supported_mode($mode) {
210 return ($mode == self::MODE_APPLICATION || $mode == self::MODE_SESSION);
211 }
212
2e638e3d
SH
213 /**
214 * Returns true if this store is making use of multiple identifiers.
215 * @return bool
216 */
758dbdf8 217 public function supports_multiple_identifiers() {
2e638e3d
SH
218 return $this->extendedmode;
219 }
220
2e638e3d
SH
221 /**
222 * Retrieves an item from the cache store given its key.
223 *
224 * @param string $key The key to retrieve
225 * @return mixed The data that was associated with the key, or false if the key did not exist.
226 */
227 public function get($key) {
228 if (!is_array($key)) {
229 $key = array('key' => $key);
230 }
170f821b 231
2e638e3d
SH
232 $result = $this->collection->findOne($key);
233 if ($result === null || !array_key_exists('data', $result)) {
234 return false;
235 }
236 $data = @unserialize($result['data']);
237 return $data;
238 }
239
240 /**
241 * Retrieves several items from the cache store in a single transaction.
242 *
243 * 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.
244 *
245 * @param array $keys The array of keys to retrieve
246 * @return array An array of items from the cache.
247 */
248 public function get_many($keys) {
249 if ($this->extendedmode) {
250 $query = $this->get_many_extendedmode_query($keys);
251 $keyarray = array();
252 foreach ($keys as $key) {
253 $keyarray[] = $key['key'];
254 }
255 $keys = $keyarray;
256 $query = array('key' => array('$in' => $keys));
257 } else {
258 $query = array('key' => array('$in' => $keys));
259 }
260 $cursor = $this->collection->find($query);
261 $results = array();
262 foreach ($cursor as $result) {
263 if (array_key_exists('key', $result)) {
264 $id = $result[$key];
265 } else {
266 $id = (string)$result['key'];
267 }
268 $results[$id] = unserialize($result['data']);
269 }
270 foreach ($keys as $key) {
271 if (!array_key_exists($key, $results)) {
272 $results[$key] = false;
273 }
274 }
275 return $results;
276 }
277
278 /**
279 * Sets an item in the cache given its key and data value.
280 *
281 * @param string $key The key to use.
282 * @param mixed $data The data to set.
283 * @return bool True if the operation was a success false otherwise.
284 */
285 public function set($key, $data) {
286 if (!is_array($key)) {
287 $record = array(
288 'key' => $key
289 );
290 } else {
291 $record = $key;
292 }
293 $record['data'] = serialize($data);
294 $options = array(
295 'upsert' => true,
296 'safe' => $this->usesafe
297 );
298 $this->delete($key);
299 $result = $this->collection->insert($record, $options);
300 return $result;
301 }
302
303 /**
304 * Sets many items in the cache in a single transaction.
305 *
306 * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
307 * keys, 'key' and 'value'.
308 * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
309 * sent ... if they care that is.
310 */
311 public function set_many(array $keyvaluearray) {
312 $count = 0;
313 foreach ($keyvaluearray as $pair) {
314 $result = $this->set($pair['key'], $pair['value']);
315 if ($result === true || (is_array($result)) && !empty($result['ok'])) {
316 $count++;
317 }
318 }
319 return;
320 }
321
322 /**
323 * Deletes an item from the cache store.
324 *
325 * @param string $key The key to delete.
326 * @return bool Returns true if the operation was a success, false otherwise.
327 */
328 public function delete($key) {
329 if (!is_array($key)) {
330 $criteria = array(
331 'key' => $key
332 );
333 } else {
334 $criteria = $key;
335 }
336 $options = array(
337 'justOne' => false,
338 'safe' => $this->usesafe
339 );
340 $result = $this->collection->remove($criteria, $options);
341 if ($result === false || (is_array($result) && !array_key_exists('ok', $result)) || $result === 0) {
342 return false;
343 }
344 return !empty($result['ok']);
345 }
346
347 /**
348 * Deletes several keys from the cache in a single action.
349 *
350 * @param array $keys The keys to delete
351 * @return int The number of items successfully deleted.
352 */
353 public function delete_many(array $keys) {
354 $count = 0;
355 foreach ($keys as $key) {
356 if ($this->delete($key)) {
357 $count++;
358 }
359 }
360 return $count;
361 }
362
363 /**
364 * Purges the cache deleting all items within it.
365 *
366 * @return boolean True on success. False otherwise.
367 */
368 public function purge() {
369 $this->collection->drop();
370 $this->collection = $this->database->selectCollection($this->definitionhash);
371 }
372
373 /**
374 * Takes the object from the add instance store and creates a configuration array that can be used to initialise an instance.
375 *
376 * @param stdClass $data
377 * @return array
378 */
379 public static function config_get_configuration_array($data) {
380 $return = array(
381 'server' => $data->server,
382 'database' => $data->database,
383 'extendedmode' => (!empty($data->extendedmode))
384 );
385 if (!empty($data->username)) {
386 $return['username'] = $data->username;
387 }
388 if (!empty($data->password)) {
389 $return['password'] = $data->password;
390 }
391 if (!empty($data->replicaset)) {
392 $return['replicaset'] = $data->replicaset;
393 }
394 if (!empty($data->usesafe)) {
395 $return['usesafe'] = true;
396 if (!empty($data->usesafevalue)) {
397 $return['usesafe'] = (int)$data->usesafevalue;
d837df0d 398 $return['usesafevalue'] = $return['usesafe'];
2e638e3d
SH
399 }
400 }
401 return $return;
402 }
403
81ede547
SH
404 /**
405 * Allows the cache store to set its data against the edit form before it is shown to the user.
406 *
407 * @param moodleform $editform
408 * @param array $config
409 */
410 public static function config_set_edit_form_data(moodleform $editform, array $config) {
411 $data = array();
412 if (!empty($config['server'])) {
413 $data['server'] = $config['server'];
414 }
415 if (!empty($config['database'])) {
416 $data['database'] = $config['database'];
417 }
d837df0d
SH
418 if (isset($config['extendedmode'])) {
419 $data['extendedmode'] = (bool)$config['extendedmode'];
81ede547
SH
420 }
421 if (!empty($config['username'])) {
422 $data['username'] = $config['username'];
423 }
424 if (!empty($config['password'])) {
425 $data['password'] = $config['password'];
426 }
427 if (!empty($config['replicaset'])) {
428 $data['replicaset'] = $config['replicaset'];
429 }
d837df0d
SH
430 if (isset($config['usesafevalue'])) {
431 $data['usesafe'] = true;
432 $data['usesafevalue'] = (int)$data['usesafe'];
433 } else if (isset($config['usesafe'])) {
434 $data['usesafe'] = (bool)$config['usesafe'];
81ede547
SH
435 }
436 $editform->set_data($data);
437 }
438
2e638e3d
SH
439 /**
440 * Performs any necessary clean up when the store instance is being deleted.
441 */
442 public function cleanup() {
443 $this->purge();
444 }
445
446 /**
447 * Generates an instance of the cache store that can be used for testing.
448 *
449 * @param cache_definition $definition
450 * @return false
451 */
452 public static function initialise_test_instance(cache_definition $definition) {
453 if (!self::are_requirements_met()) {
454 return false;
455 }
456
6fec1820 457 $config = get_config('cachestore_mongodb');
2e638e3d
SH
458 if (empty($config->testserver)) {
459 return false;
460 }
461
462 $configuration = array();
463 $configuration['server'] = $config->testserver;
464 if (!empty($config->testreplicaset)) {
465 $configuration['replicaset'] = $config->testreplicaset;
466 }
467 if (!empty($config->testusername)) {
468 $configuration['username'] = $config->testusername;
469 }
470 if (!empty($config->testpassword)) {
471 $configuration['password'] = $config->testpassword;
472 }
473 if (!empty($config->testdatabase)) {
474 $configuration['database'] = $config->testdatabase;
475 }
476 if (!empty($config->testusesafe)) {
477 $configuration['usesafe'] = $config->testusesafe;
478 }
479 if (!empty($config->testextendedmode)) {
480 $configuration['extendedmode'] = (bool)$config->testextendedmode;
481 }
482
6fec1820 483 $store = new cachestore_mongodb('Test mongodb', $configuration);
2e638e3d
SH
484 $store->initialise($definition);
485
486 return $store;
487 }
34c84c72
SH
488
489 /**
490 * Returns the name of this instance.
491 * @return string
492 */
493 public function my_name() {
494 return $this->name;
495 }
a3f3ea26 496}