on-demand release 4.5dev+
[moodle.git] / cache / classes / config.php
blob85e2a0341bbab6792199ba2c30509ec170c4b3d0
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.
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 namespace core_cache;
19 use core_cache\exception\cache_exception;
21 /**
22 * Cache configuration reader.
24 * This class is used to interact with the cache's configuration.
25 * The configuration is stored in the Moodle data directory.
27 * @package core_cache
28 * @category cache
29 * @copyright 2012 Sam Hemelryk
30 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32 class config {
33 /**
34 * The configured stores
35 * @var array
37 protected $configstores = [];
39 /**
40 * The configured mode mappings
41 * @var array
43 protected $configmodemappings = [];
45 /**
46 * The configured definitions as picked up from cache.php files
47 * @var array
49 protected $configdefinitions = [];
51 /**
52 * The definition mappings that have been configured.
53 * @var array
55 protected $configdefinitionmappings = [];
57 /**
58 * An array of configured cache lock instances.
59 * @var array
61 protected $configlocks = [];
63 /**
64 * The site identifier used when the cache config was last saved.
65 * @var string
67 protected $siteidentifier = null;
69 /**
70 * Please use config::instance to get an instance of the cache config that is ready to be used.
72 public function __construct() {
73 // Nothing to do here but look pretty.
76 /**
77 * Gets an instance of the cache config class.
79 * @return self
81 public static function instance() {
82 $factory = factory::instance();
83 return $factory->create_config_instance();
86 /**
87 * Checks if the configuration file exists.
89 * @return bool True if it exists
91 public static function config_file_exists() {
92 // Allow for late static binding by using static.
93 return file_exists(static::get_config_file_path());
96 /**
97 * Returns the expected path to the configuration file.
99 * @return string The absolute path
101 protected static function get_config_file_path() {
102 global $CFG;
103 if (!empty($CFG->altcacheconfigpath)) {
104 $path = $CFG->altcacheconfigpath;
105 if (is_dir($path) && is_writable($path)) {
106 // Its a writable directory, thats fine.
107 return $path . '/cacheconfig.php';
108 } else if (is_writable(dirname($path)) && (!file_exists($path) || is_writable($path))) {
109 // Its a file, either it doesn't exist and the directory is writable or the file exists and is writable.
110 return $path;
113 // Return the default location within dataroot.
114 return $CFG->dataroot . '/muc/config.php';
118 * Loads the configuration file and parses its contents into the expected structure.
120 * @param array|false $configuration Can be used to force a configuration. Should only be used when truly required.
121 * @return boolean
123 public function load($configuration = false) {
124 global $CFG;
126 if ($configuration === false) {
127 $configuration = $this->include_configuration();
130 $this->configstores = [];
131 $this->configdefinitions = [];
132 $this->configlocks = [];
133 $this->configmodemappings = [];
134 $this->configdefinitionmappings = [];
136 $siteidentifier = 'unknown';
137 if (array_key_exists('siteidentifier', $configuration)) {
138 $siteidentifier = $configuration['siteidentifier'];
140 $this->siteidentifier = $siteidentifier;
142 // Filter the lock instances.
143 $defaultlock = null;
144 foreach ($configuration['locks'] as $conf) {
145 if (!is_array($conf)) {
146 // Something is very wrong here.
147 continue;
149 if (!array_key_exists('name', $conf)) {
150 // Not a valid definition configuration.
151 continue;
153 $name = $conf['name'];
154 if (array_key_exists($name, $this->configlocks)) {
155 debugging('Duplicate cache lock detected. This should never happen.', DEBUG_DEVELOPER);
156 continue;
158 $conf['default'] = (!empty($conf['default']));
159 if ($defaultlock === null || $conf['default']) {
160 $defaultlock = $name;
162 $this->configlocks[$name] = $conf;
165 // Filter the stores.
166 $availableplugins = helper::early_get_cache_plugins();
167 foreach ($configuration['stores'] as $store) {
168 if (!is_array($store) || !array_key_exists('name', $store) || !array_key_exists('plugin', $store)) {
169 // Not a valid instance configuration.
170 debugging('Invalid cache store in config. Missing name or plugin.', DEBUG_DEVELOPER);
171 continue;
173 $plugin = $store['plugin'];
174 $class = 'cachestore_' . $plugin;
175 $exists = array_key_exists($plugin, $availableplugins);
176 if (!$exists) {
177 // Not a valid plugin, or has been uninstalled, just skip it an carry on.
178 debugging('Invalid cache store in config. Not an available plugin.', DEBUG_DEVELOPER);
179 continue;
181 $file = $CFG->dirroot . '/cache/stores/' . $plugin . '/lib.php';
182 if (!class_exists($class) && file_exists($file)) {
183 require_once($file);
185 if (!class_exists($class)) {
186 continue;
188 if (!array_key_exists(store::class, class_parents($class))) {
189 continue;
191 if (!array_key_exists('configuration', $store) || !is_array($store['configuration'])) {
192 $store['configuration'] = [];
194 $store['class'] = $class;
195 $store['default'] = !empty($store['default']);
196 if (!array_key_exists('lock', $store) || !array_key_exists($store['lock'], $this->configlocks)) {
197 $store['lock'] = $defaultlock;
200 $this->configstores[$store['name']] = $store;
203 // Filter the definitions.
204 foreach ($configuration['definitions'] as $id => $conf) {
205 if (!is_array($conf)) {
206 // Something is very wrong here.
207 continue;
209 if (!array_key_exists('mode', $conf) || !array_key_exists('component', $conf) || !array_key_exists('area', $conf)) {
210 // Not a valid definition configuration.
211 continue;
213 if (array_key_exists($id, $this->configdefinitions)) {
214 debugging('Duplicate cache definition detected. This should never happen.', DEBUG_DEVELOPER);
215 continue;
217 $conf['mode'] = (int)$conf['mode'];
218 if ($conf['mode'] < store::MODE_APPLICATION || $conf['mode'] > store::MODE_REQUEST) {
219 // Invalid cache mode used for the definition.
220 continue;
222 if ($conf['mode'] === store::MODE_SESSION || $conf['mode'] === store::MODE_REQUEST) {
223 // We force this for session and request caches.
224 // They are only allowed to use the default as we don't want people changing them.
225 $conf['sharingoptions'] = definition::SHARING_DEFAULT;
226 $conf['selectedsharingoption'] = definition::SHARING_DEFAULT;
227 $conf['userinputsharingkey'] = '';
228 } else {
229 // Default the sharing option as it was added for 2.5.
230 // This can be removed sometime after 2.5 is the minimum version someone can upgrade from.
231 if (!isset($conf['sharingoptions'])) {
232 $conf['sharingoptions'] = definition::SHARING_DEFAULTOPTIONS;
234 // Default the selected sharing option as it was added for 2.5.
235 // This can be removed sometime after 2.5 is the minimum version someone can upgrade from.
236 if (!isset($conf['selectedsharingoption'])) {
237 $conf['selectedsharingoption'] = definition::SHARING_DEFAULT;
239 // Default the user input sharing key as it was added for 2.5.
240 // This can be removed sometime after 2.5 is the minimum version someone can upgrade from.
241 if (!isset($conf['userinputsharingkey'])) {
242 $conf['userinputsharingkey'] = '';
245 $this->configdefinitions[$id] = $conf;
248 // Filter the mode mappings.
249 foreach ($configuration['modemappings'] as $mapping) {
250 if (!is_array($mapping) || !array_key_exists('mode', $mapping) || !array_key_exists('store', $mapping)) {
251 // Not a valid mapping configuration.
252 debugging('A cache mode mapping entry is invalid.', DEBUG_DEVELOPER);
253 continue;
255 if (!array_key_exists($mapping['store'], $this->configstores)) {
256 // Mapped array instance doesn't exist.
257 debugging('A cache mode mapping exists for a mode or store that does not exist.', DEBUG_DEVELOPER);
258 continue;
260 $mapping['mode'] = (int)$mapping['mode'];
261 if ($mapping['mode'] < 0 || $mapping['mode'] > 4) {
262 // Invalid cache type used for the mapping.
263 continue;
265 if (!array_key_exists('sort', $mapping)) {
266 $mapping['sort'] = 0;
268 $this->configmodemappings[] = $mapping;
271 // Filter the definition mappings.
272 foreach ($configuration['definitionmappings'] as $mapping) {
273 if (!is_array($mapping) || !array_key_exists('definition', $mapping) || !array_key_exists('store', $mapping)) {
274 // Not a valid mapping configuration.
275 continue;
277 if (!array_key_exists($mapping['store'], $this->configstores)) {
278 // Mapped array instance doesn't exist.
279 continue;
281 if (!array_key_exists($mapping['definition'], $this->configdefinitions)) {
282 // Mapped array instance doesn't exist.
283 continue;
285 if (!array_key_exists('sort', $mapping)) {
286 $mapping['sort'] = 0;
288 $this->configdefinitionmappings[] = $mapping;
291 usort($this->configmodemappings, [$this, 'sort_mappings']);
292 usort($this->configdefinitionmappings, [$this, 'sort_mappings']);
294 return true;
298 * Returns the site identifier used by the cache API.
299 * @return string
301 public function get_site_identifier() {
302 return $this->siteidentifier;
306 * Includes the configuration file and makes sure it contains the expected bits.
308 * You need to ensure that the config file exists before this is called.
310 * @return array
311 * @throws cache_exception
313 protected function include_configuration() {
314 $configuration = null;
315 // We need to allow for late static bindings to allow for class path mudling happending for unit tests.
316 $cachefile = static::get_config_file_path();
318 if (!file_exists($cachefile)) {
319 throw new cache_exception('Default cache config could not be found. It should have already been created by now.');
322 if (!include($cachefile)) {
323 throw new cache_exception('Unable to load the cache configuration file');
326 if (!is_array($configuration)) {
327 throw new cache_exception('Invalid cache configuration file');
329 if (!array_key_exists('stores', $configuration) || !is_array($configuration['stores'])) {
330 $configuration['stores'] = [];
332 if (!array_key_exists('modemappings', $configuration) || !is_array($configuration['modemappings'])) {
333 $configuration['modemappings'] = [];
335 if (!array_key_exists('definitions', $configuration) || !is_array($configuration['definitions'])) {
336 $configuration['definitions'] = [];
338 if (!array_key_exists('definitionmappings', $configuration) || !is_array($configuration['definitionmappings'])) {
339 $configuration['definitionmappings'] = [];
341 if (!array_key_exists('locks', $configuration) || !is_array($configuration['locks'])) {
342 $configuration['locks'] = [];
345 return $configuration;
349 * Used to sort cache config arrays based upon a sort key.
351 * Highest number at the top.
353 * @param array $a
354 * @param array $b
355 * @return int
357 protected function sort_mappings(array $a, array $b) {
358 if ($a['sort'] == $b['sort']) {
359 return 0;
361 return ($a['sort'] < $b['sort']) ? 1 : -1;
365 * Gets a definition from the config given its name.
367 * @param string $id
368 * @return bool
370 public function get_definition_by_id($id) {
371 if (array_key_exists($id, $this->configdefinitions)) {
372 return $this->configdefinitions[$id];
374 return false;
378 * Returns all the known definitions.
380 * @return array
382 public function get_definitions() {
383 return $this->configdefinitions;
387 * Returns the definitions mapped into the given store name.
389 * @param string $storename
390 * @return array Associative array of definitions, id=>definition
392 public function get_definitions_by_store($storename) {
393 $definitions = [];
395 // This function was accidentally made static at some stage in the past.
396 // It was converted to an instance method but to be backwards compatible
397 // we must step around this in code.
398 if (!isset($this)) {
399 $config = self::instance();
400 } else {
401 $config = $this;
404 $stores = $config->get_all_stores();
405 if (!array_key_exists($storename, $stores)) {
406 // The store does not exist.
407 return false;
410 $defmappings = $config->get_definition_mappings();
411 // Create an associative array for the definition mappings.
412 $thedefmappings = [];
413 foreach ($defmappings as $defmapping) {
414 $thedefmappings[$defmapping['definition']] = $defmapping;
417 // Search for matches in default mappings.
418 $defs = $config->get_definitions();
419 foreach ($config->get_mode_mappings() as $modemapping) {
420 if ($modemapping['store'] !== $storename) {
421 continue;
423 foreach ($defs as $id => $definition) {
424 if ($definition['mode'] !== $modemapping['mode']) {
425 continue;
427 // Exclude custom definitions mapping: they will be managed few lines below.
428 if (array_key_exists($id, $thedefmappings)) {
429 continue;
431 $definitions[$id] = $definition;
435 // Search for matches in the custom definitions mapping.
436 foreach ($defmappings as $defmapping) {
437 if ($defmapping['store'] !== $storename) {
438 continue;
440 $definition = $config->get_definition_by_id($defmapping['definition']);
441 if ($definition) {
442 $definitions[$defmapping['definition']] = $definition;
446 return $definitions;
450 * Returns all of the stores that are suitable for the given mode and requirements.
452 * @param int $mode One of store::MODE_*
453 * @param int $requirements The requirements of the cache as a binary flag
454 * @return array An array of suitable stores.
456 public function get_stores($mode, $requirements = 0) {
457 $stores = [];
458 foreach ($this->configstores as $name => $store) {
459 // If the mode is supported and all of the requirements are provided features.
460 if (($store['modes'] & $mode) && ($store['features'] & $requirements) === $requirements) {
461 $stores[$name] = $store;
464 return $stores;
468 * Gets all of the stores that are to be used for the given definition.
470 * @param definition $definition
471 * @return array<store>
473 public function get_stores_for_definition(definition $definition) {
474 // Check if MUC has been disabled.
475 $factory = factory::instance();
476 if ($factory->stores_disabled()) {
477 // Yip its been disabled.
478 // To facilitate this we are going to always return an empty array of stores to use.
479 // This will force all cache instances to use the cachestore_dummy.
480 // MUC will still be used essentially so that code using it will still continue to function but because no cache stores
481 // are being used interaction with MUC will be purely based around a static var.
482 return [];
485 $availablestores = $this->get_stores($definition->get_mode(), $definition->get_requirements_bin());
486 $stores = [];
487 $id = $definition->get_id();
489 // Now get any mappings and give them priority.
490 foreach ($this->configdefinitionmappings as $mapping) {
491 if ($mapping['definition'] !== $id) {
492 continue;
494 $storename = $mapping['store'];
495 if (!array_key_exists($storename, $availablestores)) {
496 continue;
498 if (array_key_exists($storename, $stores)) {
499 $store = $stores[$storename];
500 unset($stores[$storename]);
501 $stores[$storename] = $store;
502 } else {
503 $stores[$storename] = $availablestores[$storename];
507 if (empty($stores) && !$definition->is_for_mappings_only()) {
508 $mode = $definition->get_mode();
509 // Load the default stores.
510 foreach ($this->configmodemappings as $mapping) {
511 if ($mapping['mode'] === $mode && array_key_exists($mapping['store'], $availablestores)) {
512 $store = $availablestores[$mapping['store']];
513 if (empty($store['mappingsonly'])) {
514 $stores[$mapping['store']] = $store;
520 return $stores;
524 * Returns all of the configured stores
525 * @return array
527 public function get_all_stores() {
528 return $this->configstores;
532 * Returns all of the configured mode mappings
533 * @return array
535 public function get_mode_mappings() {
536 return $this->configmodemappings;
540 * Returns all of the known definition mappings.
541 * @return array
543 public function get_definition_mappings() {
544 return $this->configdefinitionmappings;
548 * Returns an array of the configured locks.
549 * @return array Array of name => config
551 public function get_locks() {
552 return $this->configlocks;
556 * Returns the lock store configuration to use with a given store.
557 * @param string $storename
558 * @return array
559 * @throws cache_exception
561 public function get_lock_for_store($storename) {
562 if (array_key_exists($storename, $this->configstores)) {
563 if (array_key_exists($this->configstores[$storename]['lock'], $this->configlocks)) {
564 $lock = $this->configstores[$storename]['lock'];
565 return $this->configlocks[$lock];
568 return $this->get_default_lock();
572 * Gets the default lock instance.
574 * @return array
575 * @throws cache_exception
577 public function get_default_lock() {
578 foreach ($this->configlocks as $lockconf) {
579 if (!empty($lockconf['default'])) {
580 return $lockconf;
583 throw new cache_exception('ex_nodefaultlock');
587 // Alias this class to the old name.
588 // This file will be autoloaded by the legacyclasses autoload system.
589 // In future all uses of this class will be corrected and the legacy references will be removed.
590 class_alias(config::class, \cache_config::class);