forked from moodle/moodle
-
Notifications
You must be signed in to change notification settings - Fork 0
/
factory.php
693 lines (638 loc) · 26.7 KB
/
factory.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains the cache factory class.
*
* This file is part of Moodle's cache API, affectionately called MUC.
* It contains the components that are requried in order to use caching.
*
* @package core
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* The cache factory class.
*
* This factory class is important because it stores instances of objects used by the cache API and returns them upon requests.
* This allows us to both reuse objects saving on overhead, and gives us an easy place to "reset" the cache API in situations that
* we need such as unit testing.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_factory {
/** The cache has not been initialised yet. */
const STATE_UNINITIALISED = 0;
/** The cache is in the process of initialising itself. */
const STATE_INITIALISING = 1;
/** The cache is in the process of saving its configuration file. */
const STATE_SAVING = 2;
/** The cache is ready to use. */
const STATE_READY = 3;
/** The cache is currently updating itself */
const STATE_UPDATING = 4;
/** The cache encountered an error while initialising. */
const STATE_ERROR_INITIALISING = 9;
/** The cache has been disabled. */
const STATE_DISABLED = 10;
/** The cache stores have been disabled */
const STATE_STORES_DISABLED = 11;
/**
* An instance of the cache_factory class created upon the first request.
* @var cache_factory
*/
protected static $instance;
/**
* An array containing caches created for definitions
* @var array
*/
protected $cachesfromdefinitions = array();
/**
* Array of caches created by parameters, ad-hoc definitions will have been used.
* @var array
*/
protected $cachesfromparams = array();
/**
* An array of stores organised by definitions.
* @var array
*/
protected $definitionstores = array();
/**
* An array of instantiated stores.
* @var array
*/
protected $stores = array();
/**
* An array of configuration instances
* @var array
*/
protected $configs = array();
/**
* An array of initialised definitions
* @var array
*/
protected $definitions = array();
/**
* An array of lock plugins.
* @var array
*/
protected $lockplugins = array();
/**
* The current state of the cache API.
* @var int
*/
protected $state = 0;
/**
* The current cache display helper.
* @var core_cache\local\administration_display_helper
*/
protected static $displayhelper = null;
/**
* Returns an instance of the cache_factory class.
*
* @param bool $forcereload If set to true a new cache_factory instance will be created and used.
* @return cache_factory
*/
public static function instance($forcereload = false) {
global $CFG;
if ($forcereload || self::$instance === null) {
// Initialise a new factory to facilitate our needs.
if (defined('CACHE_DISABLE_ALL') && CACHE_DISABLE_ALL !== false) {
// The cache has been disabled. Load disabledlib and start using the factory designed to handle this
// situation. It will use disabled alternatives where available.
require_once($CFG->dirroot.'/cache/disabledlib.php');
self::$instance = new cache_factory_disabled();
} else if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST) || defined('BEHAT_SITE_RUNNING')) {
// We're using the test factory.
require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
self::$instance = new cache_phpunit_factory();
if (defined('CACHE_DISABLE_STORES') && CACHE_DISABLE_STORES !== false) {
// The cache stores have been disabled.
self::$instance->set_state(self::STATE_STORES_DISABLED);
}
} else if (!empty($CFG->alternative_cache_factory_class)) {
$factoryclass = $CFG->alternative_cache_factory_class;
self::$instance = new $factoryclass();
} else {
// We're using the regular factory.
self::$instance = new cache_factory();
if (defined('CACHE_DISABLE_STORES') && CACHE_DISABLE_STORES !== false) {
// The cache stores have been disabled.
self::$instance->set_state(self::STATE_STORES_DISABLED);
}
}
}
return self::$instance;
}
/**
* Protected constructor, please use the static instance method.
*/
protected function __construct() {
// Nothing to do here.
}
/**
* Resets the arrays containing instantiated caches, stores, and config instances.
*/
public static function reset() {
$factory = self::instance();
$factory->reset_cache_instances();
$factory->configs = array();
$factory->definitions = array();
$factory->definitionstores = array();
$factory->lockplugins = array(); // MUST be null in order to force its regeneration.
// Reset the state to uninitialised.
$factory->state = self::STATE_UNINITIALISED;
}
/**
* Resets the stores, clearing the array of created stores.
*
* Cache objects still held onto by the code that initialised them will remain as is
* however all future requests for a cache/store will lead to a new instance being re-initialised.
*/
public function reset_cache_instances() {
$this->cachesfromdefinitions = array();
$this->cachesfromparams = array();
$this->stores = array();
}
/**
* Creates a cache object given the parameters for a definition.
*
* If a cache has already been created for the given definition then that cache instance will be returned.
*
* @param string $component
* @param string $area
* @param array $identifiers
* @param string $unused Used to be data source aggregate however that was removed and this is now unused.
* @return cache_application|cache_session|cache_request
*/
public function create_cache_from_definition($component, $area, array $identifiers = array(), $unused = null) {
$identifierstring = empty($identifiers) ? '' : '/'.http_build_query($identifiers);
$definitionname = $component.'/'.$area.$identifierstring;
if (isset($this->cachesfromdefinitions[$definitionname])) {
$cache = $this->cachesfromdefinitions[$definitionname];
return $cache;
}
$definition = $this->create_definition($component, $area);
// Identifiers are cached as part of the cache creation, so we store a cloned version of the cache.
$cacheddefinition = clone($definition);
$cacheddefinition->set_identifiers($identifiers);
$cache = $this->create_cache($cacheddefinition);
// Loaders are always held onto to speed up subsequent requests.
$this->cachesfromdefinitions[$definitionname] = $cache;
return $cache;
}
/**
* Creates an ad-hoc cache from the given param.
*
* If a cache has already been created using the same params then that cache instance will be returned.
*
* @param int $mode
* @param string $component
* @param string $area
* @param array $identifiers
* @param array $options An array of options, available options are:
* - simplekeys : Set to true if the keys you will use are a-zA-Z0-9_
* - simpledata : Set to true if the type of the data you are going to store is scalar, or an array of scalar vars
* - staticacceleration : If set to true the cache will hold onto data passing through it.
* - staticaccelerationsize : The maximum number of items to hold onto for acceleration purposes.
* @return cache_application|cache_session|cache_request
*/
public function create_cache_from_params($mode, $component, $area, array $identifiers = array(), array $options = array()) {
$identifierstring = empty($identifiers) ? '' : '_'.http_build_query($identifiers);
$key = "{$mode}_{$component}_{$area}{$identifierstring}";
if (isset($this->cachesfromparams[$key])) {
return $this->cachesfromparams[$key];
}
// Regular cache definitions are cached inside create_definition(). This is not the case for Adhoc definitions
// using load_adhoc(). They are built as a new object on each call.
// We do not need to clone the definition because we know it's new.
$definition = cache_definition::load_adhoc($mode, $component, $area, $options);
$definition->set_identifiers($identifiers);
$cache = $this->create_cache($definition);
$this->cachesfromparams[$key] = $cache;
return $cache;
}
/**
* Common public method to create a cache instance given a definition.
*
* This is used by the static make methods.
*
* @param cache_definition $definition
* @return cache_application|cache_session|cache_store
* @throws coding_exception
*/
public function create_cache(cache_definition $definition) {
$class = $definition->get_cache_class();
$stores = cache_helper::get_stores_suitable_for_definition($definition);
foreach ($stores as $key => $store) {
if (!$store::are_requirements_met()) {
unset($stores[$key]);
}
}
if (count($stores) === 0) {
// Hmm still no stores, better provide a dummy store to mimic functionality. The dev will be none the wiser.
$stores[] = $this->create_dummy_store($definition);
}
$loader = null;
if ($definition->has_data_source()) {
$loader = $definition->get_data_source();
}
while (($store = array_pop($stores)) !== null) {
$loader = new $class($definition, $store, $loader);
}
return $loader;
}
/**
* Creates a store instance given its name and configuration.
*
* If the store has already been instantiated then the original object will be returned. (reused)
*
* @param string $name The name of the store (must be unique remember)
* @param array $details
* @param cache_definition $definition The definition to instantiate it for.
* @return boolean|cache_store
*/
public function create_store_from_config($name, array $details, cache_definition $definition) {
if (!array_key_exists($name, $this->stores)) {
// Properties: name, plugin, configuration, class.
$class = $details['class'];
if (!$class::are_requirements_met()) {
return false;
}
$store = new $class($details['name'], $details['configuration']);
$this->stores[$name] = $store;
}
/* @var cache_store $store */
$store = $this->stores[$name];
// We check are_requirements_met although we expect is_ready is going to check as well.
if (!$store::are_requirements_met() || !$store->is_ready() || !$store->is_supported_mode($definition->get_mode())) {
return false;
}
// We always create a clone of the original store.
// If we were to clone a store that had already been initialised with a definition then
// we'd run into a myriad of issues.
// We use a method of the store to create a clone rather than just creating it ourselves
// so that if any store out there doesn't handle cloning they can override this method in
// order to address the issues.
$store = $this->stores[$name]->create_clone($details);
$store->initialise($definition);
$definitionid = $definition->get_id();
if (!isset($this->definitionstores[$definitionid])) {
$this->definitionstores[$definitionid] = array();
}
$this->definitionstores[$definitionid][] = $store;
return $store;
}
/**
* Returns an array of cache stores that have been initialised for use in definitions.
* @param cache_definition $definition
* @return array
*/
public function get_store_instances_in_use(cache_definition $definition) {
$id = $definition->get_id();
if (!isset($this->definitionstores[$id])) {
return array();
}
return $this->definitionstores[$id];
}
/**
* Returns the cache instances that have been used within this request.
* @since Moodle 2.6
* @return array
*/
public function get_caches_in_use() {
return $this->cachesfromdefinitions;
}
/**
* Gets all adhoc caches that have been used within this request.
*
* @return cache_store[] Caches currently in use
*/
public function get_adhoc_caches_in_use() {
return $this->cachesfromparams;
}
/**
* Creates a cache config instance with the ability to write if required.
*
* @param bool $writer If set to true an instance that can update the configuration will be returned.
* @return cache_config|cache_config_writer
*/
public function create_config_instance($writer = false) {
global $CFG;
// The class to use.
$class = 'cache_config';
// Are we running tests of some form?
$testing = (defined('PHPUNIT_TEST') && PHPUNIT_TEST) || defined('BEHAT_SITE_RUNNING');
// Check if this is a PHPUnit test and redirect to the phpunit config classes if it is.
if ($testing) {
require_once($CFG->dirroot.'/cache/locallib.php');
require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
// We have just a single class for PHP unit tests. We don't care enough about its
// performance to do otherwise and having a single method allows us to inject things into it
// while testing.
$class = 'cache_config_testing';
}
// Check if we need to create a config file with defaults.
$needtocreate = !$class::config_file_exists();
if ($writer || $needtocreate) {
require_once($CFG->dirroot.'/cache/locallib.php');
if (!$testing) {
$class .= '_writer';
}
}
$error = false;
if ($needtocreate) {
// Create the default configuration.
// Update the state, we are now initialising the cache.
self::set_state(self::STATE_INITIALISING);
/** @var cache_config_writer $class */
$configuration = $class::create_default_configuration();
if ($configuration !== true) {
// Failed to create the default configuration. Disable the cache stores and update the state.
self::set_state(self::STATE_ERROR_INITIALISING);
$this->configs[$class] = new $class;
$this->configs[$class]->load($configuration);
$error = true;
}
}
if (!array_key_exists($class, $this->configs)) {
// Create a new instance and call it to load it.
$this->configs[$class] = new $class;
$this->configs[$class]->load();
}
if (!$error) {
// The cache is now ready to use. Update the state.
self::set_state(self::STATE_READY);
}
// Return the instance.
return $this->configs[$class];
}
/**
* Creates a definition instance or returns the existing one if it has already been created.
* @param string $component
* @param string $area
* @param string $unused This used to be data source aggregate - however that functionality has been removed and
* this argument is now unused.
* @return cache_definition
* @throws coding_exception If the definition cannot be found.
*/
public function create_definition($component, $area, $unused = null) {
$id = $component.'/'.$area;
if (!isset($this->definitions[$id])) {
// This is the first time this definition has been requested.
if ($this->is_initialising()) {
// We're initialising the cache right now. Don't try to create another config instance.
// We'll just use an ad-hoc cache for the time being.
$definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, $component, $area);
} else {
// Load all the known definitions and find the desired one.
$instance = $this->create_config_instance();
$definition = $instance->get_definition_by_id($id);
if (!$definition) {
// Oh-oh the definition doesn't exist.
// There are several things that could be going on here.
// We may be installing/upgrading a site and have hit a definition that hasn't been used before.
// Of the developer may be trying to use a newly created definition.
if ($this->is_updating()) {
// The cache is presently initialising and the requested cache definition has not been found.
// This means that the cache initialisation has requested something from a cache (I had recursive nightmares about this).
// To serve this purpose and avoid errors we are going to make use of an ad-hoc cache rather than
// search for the definition which would possibly cause an infitite loop trying to initialise the cache.
$definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, $component, $area);
} else {
// Either a typo of the developer has just created the definition and is using it for the first time.
$this->reset();
$instance = $this->create_config_instance(true);
$instance->update_definitions();
$definition = $instance->get_definition_by_id($id);
if (!$definition) {
throw new coding_exception('The requested cache definition does not exist.'. $id, $id);
}
if (!$this->is_disabled()) {
debugging('Cache definitions reparsed causing cache reset in order to locate definition.
You should bump the version number to ensure definitions are reprocessed.', DEBUG_DEVELOPER);
}
$definition = cache_definition::load($id, $definition);
}
} else {
$definition = cache_definition::load($id, $definition);
}
}
$this->definitions[$id] = $definition;
}
return $this->definitions[$id];
}
/**
* Creates a dummy store object for use when a loader has no potential stores to use.
*
* @param cache_definition $definition
* @return cachestore_dummy
*/
protected function create_dummy_store(cache_definition $definition) {
global $CFG;
require_once($CFG->dirroot.'/cache/classes/dummystore.php');
$store = new cachestore_dummy();
$store->initialise($definition);
return $store;
}
/**
* Returns a lock instance ready for use.
*
* @param array $config
* @return cache_lock_interface
*/
public function create_lock_instance(array $config) {
global $CFG;
if (!array_key_exists('name', $config) || !array_key_exists('type', $config)) {
throw new coding_exception('Invalid cache lock instance provided');
}
$name = $config['name'];
$type = $config['type'];
unset($config['name']);
unset($config['type']);
if (!isset($this->lockplugins[$type])) {
$pluginname = substr($type, 10);
$file = $CFG->dirroot."/cache/locks/{$pluginname}/lib.php";
if (file_exists($file) && is_readable($file)) {
require_once($file);
}
if (!class_exists($type)) {
throw new coding_exception('Invalid lock plugin requested.');
}
$this->lockplugins[$type] = $type;
}
if (!array_key_exists($type, $this->lockplugins)) {
throw new coding_exception('Invalid cache lock type.');
}
$class = $this->lockplugins[$type];
return new $class($name, $config);
}
/**
* Returns the current state of the cache API.
*
* @return int
*/
public function get_state() {
return $this->state;
}
/**
* Updates the state fo the cache API.
*
* @param int $state
* @return bool
*/
public function set_state($state) {
if ($state <= $this->state) {
return false;
}
$this->state = $state;
return true;
}
/**
* Informs the factory that the cache is currently updating itself.
*
* This forces the state to upgrading and can only be called once the cache is ready to use.
* Calling it ensure we don't try to reinstantite things when requesting cache definitions that don't exist yet.
*/
public function updating_started() {
if ($this->state !== self::STATE_READY) {
return false;
}
$this->state = self::STATE_UPDATING;
return true;
}
/**
* Informs the factory that the upgrading has finished.
*
* This forces the state back to ready.
*/
public function updating_finished() {
$this->state = self::STATE_READY;
}
/**
* Returns true if the cache API has been disabled.
*
* @return bool
*/
public function is_disabled() {
return $this->state === self::STATE_DISABLED;
}
/**
* Returns true if the cache is currently initialising itself.
*
* This includes both initialisation and saving the cache config file as part of that initialisation.
*
* @return bool
*/
public function is_initialising() {
return $this->state === self::STATE_INITIALISING || $this->state === self::STATE_SAVING;
}
/**
* Returns true if the cache is currently updating itself.
*
* @return bool
*/
public function is_updating() {
return $this->state === self::STATE_UPDATING;
}
/**
* Disables as much of the cache API as possible.
*
* All of the magic associated with the disabled cache is wrapped into this function.
* In switching out the factory for the disabled factory it gains full control over the initialisation of objects
* and can use all of the disabled alternatives.
* Simple!
*
* This function has been marked as protected so that it cannot be abused through the public API presently.
* Perhaps in the future we will allow this, however as per the build up to the first release containing
* MUC it was decided that this was just to risky and abusable.
*/
protected static function disable() {
global $CFG;
require_once($CFG->dirroot.'/cache/disabledlib.php');
self::$instance = new cache_factory_disabled();
}
/**
* Returns true if the cache stores have been disabled.
*
* @return bool
*/
public function stores_disabled() {
return $this->state === self::STATE_STORES_DISABLED || $this->is_disabled();
}
/**
* Disables cache stores.
*
* The cache API will continue to function however none of the actual stores will be used.
* Instead the dummy store will be provided for all cache requests.
* This is useful in situations where you cannot be sure any stores are working.
*
* In order to re-enable the cache you must call the cache factories static reset method:
* <code>
* // Disable the cache factory.
* cache_factory::disable_stores();
* // Re-enable the cache factory by resetting it.
* cache_factory::reset();
* </code>
*/
public static function disable_stores() {
// First reset to clear any static acceleration array.
$factory = self::instance();
$factory->reset_cache_instances();
$factory->set_state(self::STATE_STORES_DISABLED);
}
/**
* Returns an instance of the current display_helper.
*
* @return core_cache\administration_helper
*/
public static function get_administration_display_helper() : core_cache\administration_helper {
if (is_null(self::$displayhelper)) {
self::$displayhelper = new \core_cache\local\administration_display_helper();
}
return self::$displayhelper;
}
/**
* Gets the cache_config_writer to use when caching is disabled.
* This should only be called from cache_factory_disabled.
*
* @return cache_config_writer
*/
public static function get_disabled_writer(): cache_config_writer {
global $CFG;
// Figure out if we are in a recursive loop using late static binding.
// This happens when get_disabled_writer is not overridden. We just want the default.
$loop = false;
if (!empty($CFG->alternative_cache_factory_class)) {
$loop = get_called_class() === $CFG->alternative_cache_factory_class;
}
if (!$loop && !empty($CFG->alternative_cache_factory_class)) {
// Get the class to use from the alternative factory.
$factoryinstance = new $CFG->alternative_cache_factory_class();
return $factoryinstance::get_disabled_writer();
} else {
// We got here from cache_factory_disabled.
// We should use the default writer here.
// Make sure we have a default config if needed.
if (!cache_config::config_file_exists()) {
cache_config_writer::create_default_configuration(true);
}
return new cache_config_writer();
}
}
}