2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
19 use core_cache\exception\cache_exception
;
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.
29 * @copyright 2012 Sam Hemelryk
30 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 * The configured stores
37 protected $configstores = [];
40 * The configured mode mappings
43 protected $configmodemappings = [];
46 * The configured definitions as picked up from cache.php files
49 protected $configdefinitions = [];
52 * The definition mappings that have been configured.
55 protected $configdefinitionmappings = [];
58 * An array of configured cache lock instances.
61 protected $configlocks = [];
64 * The site identifier used when the cache config was last saved.
67 protected $siteidentifier = null;
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.
77 * Gets an instance of the cache config class.
81 public static function instance() {
82 $factory = factory
::instance();
83 return $factory->create_config_instance();
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());
97 * Returns the expected path to the configuration file.
99 * @return string The absolute path
101 protected static function get_config_file_path() {
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.
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.
123 public function load($configuration = false) {
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.
144 foreach ($configuration['locks'] as $conf) {
145 if (!is_array($conf)) {
146 // Something is very wrong here.
149 if (!array_key_exists('name', $conf)) {
150 // Not a valid definition configuration.
153 $name = $conf['name'];
154 if (array_key_exists($name, $this->configlocks
)) {
155 debugging('Duplicate cache lock detected. This should never happen.', DEBUG_DEVELOPER
);
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
);
173 $plugin = $store['plugin'];
174 $class = 'cachestore_' . $plugin;
175 $exists = array_key_exists($plugin, $availableplugins);
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
);
181 $file = $CFG->dirroot
. '/cache/stores/' . $plugin . '/lib.php';
182 if (!class_exists($class) && file_exists($file)) {
185 if (!class_exists($class)) {
188 if (!array_key_exists(store
::class, class_parents($class))) {
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.
209 if (!array_key_exists('mode', $conf) ||
!array_key_exists('component', $conf) ||
!array_key_exists('area', $conf)) {
210 // Not a valid definition configuration.
213 if (array_key_exists($id, $this->configdefinitions
)) {
214 debugging('Duplicate cache definition detected. This should never happen.', DEBUG_DEVELOPER
);
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.
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'] = '';
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
);
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
);
260 $mapping['mode'] = (int)$mapping['mode'];
261 if ($mapping['mode'] < 0 ||
$mapping['mode'] > 4) {
262 // Invalid cache type used for the mapping.
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.
277 if (!array_key_exists($mapping['store'], $this->configstores
)) {
278 // Mapped array instance doesn't exist.
281 if (!array_key_exists($mapping['definition'], $this->configdefinitions
)) {
282 // Mapped array instance doesn't exist.
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']);
298 * Returns the site identifier used by the cache API.
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.
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.
357 protected function sort_mappings(array $a, array $b) {
358 if ($a['sort'] == $b['sort']) {
361 return ($a['sort'] < $b['sort']) ?
1 : -1;
365 * Gets a definition from the config given its name.
370 public function get_definition_by_id($id) {
371 if (array_key_exists($id, $this->configdefinitions
)) {
372 return $this->configdefinitions
[$id];
378 * Returns all the known definitions.
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) {
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.
399 $config = self
::instance();
404 $stores = $config->get_all_stores();
405 if (!array_key_exists($storename, $stores)) {
406 // The store does not exist.
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) {
423 foreach ($defs as $id => $definition) {
424 if ($definition['mode'] !== $modemapping['mode']) {
427 // Exclude custom definitions mapping: they will be managed few lines below.
428 if (array_key_exists($id, $thedefmappings)) {
431 $definitions[$id] = $definition;
435 // Search for matches in the custom definitions mapping.
436 foreach ($defmappings as $defmapping) {
437 if ($defmapping['store'] !== $storename) {
440 $definition = $config->get_definition_by_id($defmapping['definition']);
442 $definitions[$defmapping['definition']] = $definition;
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) {
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;
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.
485 $availablestores = $this->get_stores($definition->get_mode(), $definition->get_requirements_bin());
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) {
494 $storename = $mapping['store'];
495 if (!array_key_exists($storename, $availablestores)) {
498 if (array_key_exists($storename, $stores)) {
499 $store = $stores[$storename];
500 unset($stores[$storename]);
501 $stores[$storename] = $store;
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;
524 * Returns all of the configured stores
527 public function get_all_stores() {
528 return $this->configstores
;
532 * Returns all of the configured mode mappings
535 public function get_mode_mappings() {
536 return $this->configmodemappings
;
540 * Returns all of the known definition mappings.
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
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.
575 * @throws cache_exception
577 public function get_default_lock() {
578 foreach ($this->configlocks
as $lockconf) {
579 if (!empty($lockconf['default'])) {
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);