MDL-36768 cache: Implemented abstract cache store base class
[moodle.git] / cache / stores / mongodb / lib.php
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/>.
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  */
27 defined('MOODLE_INTERNAL') || die();
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  */
40 class cachestore_mongodb extends cache_store_base {
42     /**
43      * The name of the store
44      * @var string
45      */
46     protected $name;
48     /**
49      * The server connection string. Comma separated values.
50      * @var string
51      */
52     protected $server = 'mongodb://127.0.0.1:27017';
54     /**
55      * The database connection options
56      * @var array
57      */
58     protected $options = array();
60     /**
61      * The name of the database to use.
62      * @var string
63      */
64     protected $databasename = 'mcache';
66     /**
67      * The Connection object
68      * @var Mongo
69      */
70     protected $connection;
72     /**
73      * The Database Object
74      * @var MongoDB
75      */
76     protected $database;
78     /**
79      * The Collection object
80      * @var MongoCollection
81      */
82     protected $collection;
84     /**
85      * Determines if and what safe setting is to be used.
86      * @var bool|int
87      */
88     protected $usesafe = false;
90     /**
91      * If set to true then multiple identifiers will be requested and used.
92      * @var bool
93      */
94     protected $extendedmode = false;
96     /**
97      * The definition has which is used in the construction of the collection.
98      * @var string
99      */
100     protected $definitionhash = null;
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;
110         if (array_key_exists('server', $configuration)) {
111             $this->server = $configuration['server'];
112         }
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         }
133         $this->isready = self::are_requirements_met();
134     }
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     }
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     }
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     }
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     }
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     }
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     }
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     }
213     /**
214      * Returns true if this store is making use of multiple identifiers.
215      * @return bool
216      */
217     public function supports_multiple_identifiers() {
218         return $this->extendedmode;
219     }
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         }
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     }
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     }
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     }
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     }
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     }
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     }
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     }
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;
398                 $return['usesafevalue'] = $return['usesafe'];
399             }
400         }
401         return $return;
402     }
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         }
418         if (isset($config['extendedmode'])) {
419             $data['extendedmode'] = (bool)$config['extendedmode'];
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         }
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'];
435         }
436         $editform->set_data($data);
437     }
439     /**
440      * Performs any necessary clean up when the store instance is being deleted.
441      */
442     public function cleanup() {
443         $this->purge();
444     }
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         }
457         $config = get_config('cachestore_mongodb');
458         if (empty($config->testserver)) {
459             return false;
460         }
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         }
483         $store = new cachestore_mongodb('Test mongodb', $configuration);
484         $store->initialise($definition);
486         return $store;
487     }
489     /**
490      * Returns the name of this instance.
491      * @return string
492      */
493     public function my_name() {
494         return $this->name;
495     }