MDL-36111 cache: remove double semicolons
[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 implements cache_store {
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 true if the user can add an instance of this store.
146      * @return bool
147      */
148     public static function can_add_instance() {
149         return true;
150     }
152     /**
153      * Returns the supported features.
154      * @param array $configuration
155      * @return int
156      */
157     public static function get_supported_features(array $configuration = array()) {
158         $supports = self::SUPPORTS_DATA_GUARANTEE;
159         if (array_key_exists('extendedmode', $configuration) && $configuration['extendedmode']) {
160             $supports += self::SUPPORTS_MULTIPLE_IDENTIFIERS;
161         }
162         return $supports;
163     }
165     /**
166      * Returns an int describing the supported modes.
167      * @param array $configuration
168      * @return int
169      */
170     public static function get_supported_modes(array $configuration = array()) {
171         return self::MODE_APPLICATION + self::MODE_SESSION;
172     }
174     /**
175      * Initialises the store instance for use.
176      *
177      * This function is reponsible for making the connection.
178      *
179      * @param cache_definition $definition
180      * @throws coding_exception
181      */
182     public function initialise(cache_definition $definition) {
183         if ($this->is_initialised()) {
184             throw new coding_exception('This mongodb instance has already been initialised.');
185         }
186         $this->definitionhash = $definition->generate_definition_hash();
187         $this->connection = new Mongo($this->server, $this->options);
188         $this->database = $this->connection->selectDB($this->databasename);
189         $this->collection = $this->database->selectCollection($this->definitionhash);
190         $this->collection->ensureIndex(array('key' => 1), array(
191             'safe' => $this->usesafe,
192             'name' => 'idx_key'
193         ));
194     }
196     /**
197      * Returns true if this store instance has been initialised.
198      * @return bool
199      */
200     public function is_initialised() {
201         return ($this->database instanceof MongoDB);
202     }
204     /**
205      * Returns true if this store instance is ready to use.
206      * @return bool
207      */
208     public function is_ready() {
209         return $this->isready;
210     }
212     /**
213      * Returns true if the given mode is supported by this store.
214      * @param int $mode
215      * @return bool
216      */
217     public static function is_supported_mode($mode) {
218         return ($mode == self::MODE_APPLICATION || $mode == self::MODE_SESSION);
219     }
221     /**
222      * Returns true if this store guarantees its data is there once set.
223      * @return bool
224      */
225     public function supports_data_guarantee() {
226         return true;
227     }
229     /**
230      * Returns true if this store is making use of multiple identifiers.
231      * @return bool
232      */
233     public function supports_multiple_identifiers() {
234         return $this->extendedmode;
235     }
237     /**
238      * Returns true if this store supports native TTL.
239      * @return bool
240      */
241     public function supports_native_ttl() {
242         return false;
243     }
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         }
256         $result = $this->collection->findOne($key);
257         if ($result === null || !array_key_exists('data', $result)) {
258             return false;
259         }
260         $data = @unserialize($result['data']);
261         return $data;
262     }
264     /**
265      * Retrieves several items from the cache store in a single transaction.
266      *
267      * 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.
268      *
269      * @param array $keys The array of keys to retrieve
270      * @return array An array of items from the cache.
271      */
272     public function get_many($keys) {
273         if ($this->extendedmode) {
274             $query = $this->get_many_extendedmode_query($keys);
275             $keyarray = array();
276             foreach ($keys as $key) {
277                 $keyarray[] = $key['key'];
278             }
279             $keys = $keyarray;
280             $query = array('key' => array('$in' => $keys));
281         } else {
282             $query = array('key' => array('$in' => $keys));
283         }
284         $cursor = $this->collection->find($query);
285         $results = array();
286         foreach ($cursor as $result) {
287             if (array_key_exists('key', $result)) {
288                 $id = $result[$key];
289             } else {
290                 $id = (string)$result['key'];
291             }
292             $results[$id] = unserialize($result['data']);
293         }
294         foreach ($keys as $key) {
295             if (!array_key_exists($key, $results)) {
296                 $results[$key] = false;
297             }
298         }
299         return $results;
300     }
302     /**
303      * Sets an item in the cache given its key and data value.
304      *
305      * @param string $key The key to use.
306      * @param mixed $data The data to set.
307      * @return bool True if the operation was a success false otherwise.
308      */
309     public function set($key, $data) {
310         if (!is_array($key)) {
311             $record = array(
312                 'key' => $key
313             );
314         } else {
315             $record = $key;
316         }
317         $record['data'] = serialize($data);
318         $options = array(
319             'upsert' => true,
320             'safe' => $this->usesafe
321         );
322         $this->delete($key);
323         $result = $this->collection->insert($record, $options);
324         return $result;
325     }
327     /**
328      * Sets many items in the cache in a single transaction.
329      *
330      * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
331      *      keys, 'key' and 'value'.
332      * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
333      *      sent ... if they care that is.
334      */
335     public function set_many(array $keyvaluearray) {
336         $count = 0;
337         foreach ($keyvaluearray as $pair) {
338             $result = $this->set($pair['key'], $pair['value']);
339             if ($result === true || (is_array($result)) && !empty($result['ok'])) {
340                  $count++;
341             }
342         }
343         return;
344     }
346     /**
347      * Deletes an item from the cache store.
348      *
349      * @param string $key The key to delete.
350      * @return bool Returns true if the operation was a success, false otherwise.
351      */
352     public function delete($key) {
353         if (!is_array($key)) {
354             $criteria = array(
355                 'key' => $key
356             );
357         } else {
358             $criteria = $key;
359         }
360         $options = array(
361             'justOne' => false,
362             'safe' => $this->usesafe
363         );
364         $result = $this->collection->remove($criteria, $options);
365         if ($result === false || (is_array($result) && !array_key_exists('ok', $result)) || $result === 0) {
366             return false;
367         }
368         return !empty($result['ok']);
369     }
371     /**
372      * Deletes several keys from the cache in a single action.
373      *
374      * @param array $keys The keys to delete
375      * @return int The number of items successfully deleted.
376      */
377     public function delete_many(array $keys) {
378         $count = 0;
379         foreach ($keys as $key) {
380             if ($this->delete($key)) {
381                 $count++;
382             }
383         }
384         return $count;
385     }
387     /**
388      * Purges the cache deleting all items within it.
389      *
390      * @return boolean True on success. False otherwise.
391      */
392     public function purge() {
393         $this->collection->drop();
394         $this->collection = $this->database->selectCollection($this->definitionhash);
395     }
397     /**
398      * Takes the object from the add instance store and creates a configuration array that can be used to initialise an instance.
399      *
400      * @param stdClass $data
401      * @return array
402      */
403     public static function config_get_configuration_array($data) {
404         $return = array(
405             'server' => $data->server,
406             'database' => $data->database,
407             'extendedmode' => (!empty($data->extendedmode))
408         );
409         if (!empty($data->username)) {
410             $return['username'] = $data->username;
411         }
412         if (!empty($data->password)) {
413             $return['password'] = $data->password;
414         }
415         if (!empty($data->replicaset)) {
416             $return['replicaset'] = $data->replicaset;
417         }
418         if (!empty($data->usesafe)) {
419             $return['usesafe'] = true;
420             if (!empty($data->usesafevalue)) {
421                 $return['usesafe'] = (int)$data->usesafevalue;
422                 $return['usesafevalue'] = $return['usesafe'];
423             }
424         }
425         return $return;
426     }
428     /**
429      * Allows the cache store to set its data against the edit form before it is shown to the user.
430      *
431      * @param moodleform $editform
432      * @param array $config
433      */
434     public static function config_set_edit_form_data(moodleform $editform, array $config) {
435         $data = array();
436         if (!empty($config['server'])) {
437             $data['server'] = $config['server'];
438         }
439         if (!empty($config['database'])) {
440             $data['database'] = $config['database'];
441         }
442         if (isset($config['extendedmode'])) {
443             $data['extendedmode'] = (bool)$config['extendedmode'];
444         }
445         if (!empty($config['username'])) {
446             $data['username'] = $config['username'];
447         }
448         if (!empty($config['password'])) {
449             $data['password'] = $config['password'];
450         }
451         if (!empty($config['replicaset'])) {
452             $data['replicaset'] = $config['replicaset'];
453         }
454         if (isset($config['usesafevalue'])) {
455             $data['usesafe'] = true;
456             $data['usesafevalue'] = (int)$data['usesafe'];
457         } else if (isset($config['usesafe'])) {
458             $data['usesafe'] = (bool)$config['usesafe'];
459         }
460         $editform->set_data($data);
461     }
463     /**
464      * Performs any necessary clean up when the store instance is being deleted.
465      */
466     public function cleanup() {
467         $this->purge();
468     }
470     /**
471      * Generates an instance of the cache store that can be used for testing.
472      *
473      * @param cache_definition $definition
474      * @return false
475      */
476     public static function initialise_test_instance(cache_definition $definition) {
477         if (!self::are_requirements_met()) {
478             return false;
479         }
481         $config = get_config('cachestore_mongodb');
482         if (empty($config->testserver)) {
483             return false;
484         }
486         $configuration = array();
487         $configuration['server'] = $config->testserver;
488         if (!empty($config->testreplicaset)) {
489             $configuration['replicaset'] = $config->testreplicaset;
490         }
491         if (!empty($config->testusername)) {
492             $configuration['username'] = $config->testusername;
493         }
494         if (!empty($config->testpassword)) {
495             $configuration['password'] = $config->testpassword;
496         }
497         if (!empty($config->testdatabase)) {
498             $configuration['database'] = $config->testdatabase;
499         }
500         if (!empty($config->testusesafe)) {
501             $configuration['usesafe'] = $config->testusesafe;
502         }
503         if (!empty($config->testextendedmode)) {
504             $configuration['extendedmode'] = (bool)$config->testextendedmode;
505         }
507         $store = new cachestore_mongodb('Test mongodb', $configuration);
508         $store->initialise($definition);
510         return $store;
511     }
513     /**
514      * Returns the name of this instance.
515      * @return string
516      */
517     public function my_name() {
518         return $this->name;
519     }