MDL-39312 cache: interfaces for managing lock instances.

Bug: There was no way to create an instance of a lock plugin for use
within Moodle.
Solution: Implemented management interfaces as part of cache/admin.php
to allow for instances to be added and deleted.
This was done in along the same lines of adding store instances.
This commit is contained in:
Sam Hemelryk 2013-04-30 16:26:34 +12:00
parent cf5a3296c4
commit acf49f4b9a
9 changed files with 323 additions and 10 deletions

50
cache/admin.php vendored
View File

@ -84,6 +84,7 @@ if (!empty($action) && confirm_sesskey()) {
} else if ($data = $mform->get_data()) {
$config = cache_administration_helper::get_store_configuration_from_data($data);
$writer = cache_config_writer::instance();
unset($config['lock']);
foreach ($writer->get_locks() as $lock => $lockconfig) {
if ($lock == $data->lock) {
@ -180,6 +181,55 @@ if (!empty($action) && confirm_sesskey()) {
cache_helper::purge_store($store);
redirect($PAGE->url, get_string('purgestoresuccess', 'cache'), 5);
break;
case 'newlockinstance':
// Adds a new lock instance.
$lock = required_param('lock', PARAM_ALPHANUMEXT);
$mform = cache_administration_helper::get_add_lock_form($lock);
if ($mform->is_cancelled()) {
redirect($PAGE->url);
} else if ($data = $mform->get_data()) {
$factory = cache_factory::instance();
$config = $factory->create_config_instance(true);
$name = $data->name;
$data = cache_administration_helper::get_lock_configuration_from_data($lock, $data);
$config->add_lock_instance($name, $lock, $data);
redirect($PAGE->url, get_string('addlocksuccess', 'cache', $name), 5);
}
break;
case 'deletelock':
// Deletes a lock instance.
$lock = required_param('lock', PARAM_ALPHANUMEXT);
$confirm = optional_param('confirm', false, PARAM_BOOL);
if (!array_key_exists($lock, $locks)) {
$notifysuccess = false;
$notification = get_string('invalidlock');
} else if ($locks[$lock]['uses'] > 0) {
$notifysuccess = false;
$notification = get_string('deletelockhasuses', 'cache');
}
if ($notifysuccess) {
if (!$confirm) {
$title = get_string('confirmlockdeletion', 'cache');
$params = array('lock' => $lock, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
$url = new moodle_url($PAGE->url, $params);
$button = new single_button($url, get_string('deletelock', 'cache'));
$PAGE->set_title($title);
$PAGE->set_heading($SITE->fullname);
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
$confirmation = get_string('deletelockconfirmation', 'cache', $lock);
echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
echo $OUTPUT->footer();
exit;
} else {
$writer = cache_config_writer::instance();
$writer->delete_lock_instance($lock);
redirect($PAGE->url, get_string('deletelocksuccess', 'cache'), 5);
}
}
break;
}
}

View File

@ -541,6 +541,16 @@ class cache_config {
return $this->configlocks[$lock];
}
}
return $this->get_default_lock();
}
/**
* Gets the default lock instance.
*
* @return array
* @throws cache_exception
*/
public function get_default_lock() {
foreach ($this->configlocks as $lockconf) {
if (!empty($lockconf['default'])) {
return $lockconf;

76
cache/forms.php vendored
View File

@ -67,10 +67,10 @@ class cachestore_addinstance_form extends moodleform {
if (is_array($locks)) {
$form->addElement('select', 'lock', get_string('lockmethod', 'cache'), $locks);
$form->addHelpButton('lock', 'lockmethod', 'cache');
$form->setType('lock', PARAM_PLUGIN);
$form->setType('lock', PARAM_ALPHANUMEXT);
} else {
$form->addElement('hidden', 'lock', '');
$form->setType('lock', PARAM_PLUGIN);
$form->setType('lock', PARAM_ALPHANUMEXT);
$form->addElement('static', 'lock-value', get_string('lockmethod', 'cache'),
'<em>'.get_string('nativelocking', 'cache').'</em>');
}
@ -222,3 +222,75 @@ class cache_mode_mappings_form extends moodleform {
$this->add_action_buttons();
}
}
/**
* Form to add a cache lock instance.
*
* All cache lock plugins that wish to have custom configuration should override
* this form, and more explicitly the plugin_definition and plugin_validation methods.
*
* @package core
* @category cache
* @copyright 2013 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_lock_form extends moodleform {
/**
* Defines this form.
*/
final public function definition() {
$plugin = $this->_customdata['lock'];
$this->_form->addElement('hidden', 'action', 'newlockinstance');
$this->_form->setType('action', PARAM_ALPHANUMEXT);
$this->_form->addElement('hidden', 'lock', $plugin);
$this->_form->setType('lock', PARAM_COMPONENT);
$this->_form->addElement('text', 'name', get_string('lockname', 'cache'));
$this->_form->setType('name', PARAM_ALPHANUMEXT);
$this->_form->addRule('name', get_string('required'), 'required');
$this->_form->addElement('static', 'namedesc', '', get_string('locknamedesc', 'cache'));
$this->plugin_definition();
$this->add_action_buttons();
}
/**
* Validates this form.
*
* @param array $data
* @param array $files
* @return array
*/
final public function validation($data, $files) {
$errors = parent::validation($data, $files);
if (!isset($errors['name'])) {
$config = cache_config::instance();
if (in_array($data['name'], array_keys($config->get_locks()))) {
$errors['name'] = get_string('locknamenotunique', 'cache');
}
}
$errors = $this->plugin_validation($data, $files, $errors);
return $errors;
}
/**
* Plugin specific definition.
*/
public function plugin_definition() {
// No custom validation going on here.
}
/**
* Plugin specific validation.
*
* @param array $data
* @param array $files
* @param array $errors
* @return array
*/
public function plugin_validation($data, $files, array $errors) {
return $errors;
}
}

148
cache/locallib.php vendored
View File

@ -187,6 +187,68 @@ class cache_config_writer extends cache_config {
return true;
}
/**
* Adds a new lock instance to the config file.
*
* @param string $name The name the user gave the instance. PARAM_ALHPANUMEXT
* @param string $plugin The plugin we are creating an instance of.
* @param string $configuration Configuration data from the config instance.
* @throws cache_exception
*/
public function add_lock_instance($name, $plugin, $configuration = array()) {
if (array_key_exists($name, $this->configlocks)) {
throw new cache_exception('Duplicate name specificed for cache lock instance. You must provide a unique name.');
}
$class = 'cachelock_'.$plugin;
if (!class_exists($class)) {
$plugins = get_plugin_list_with_file('cachelock', 'lib.php');
if (!array_key_exists($plugin, $plugins)) {
throw new cache_exception('Invalid lock name specified. The plugin does not exist or is not valid.');
}
$file = $plugins[$plugin];
if (file_exists($file)) {
require_once($file);
}
if (!class_exists($class)) {
throw new cache_exception('Invalid lock plugin specified. The plugin does not contain the required class.');
}
}
$reflection = new ReflectionClass($class);
if (!$reflection->implementsInterface('cache_lock_interface')) {
throw new cache_exception('Invalid lock plugin specified. The plugin does not implement the required interface.');
}
$this->configlocks[$name] = array_merge($configuration, array(
'name' => $name,
'type' => 'cachelock_'.$plugin,
'default' => false
));
$this->config_save();
}
/**
* Deletes a lock instance given its name.
*
* @param string $name The name of the plugin, PARAM_ALPHANUMEXT.
* @return bool
* @throws cache_exception
*/
public function delete_lock_instance($name) {
if (!array_key_exists($name, $this->configlocks)) {
throw new cache_exception('The requested store does not exist.');
}
if ($this->configlocks[$name]['default']) {
throw new cache_exception('You can not delete the default lock.');
}
foreach ($this->configstores as $store) {
if (isset($store['lock']) && $store['lock'] === $name) {
throw new cache_exception('You cannot delete a cache lock that is being used by a store.');
}
}
unset($this->configlocks[$name]);
$this->config_save();
return true;
}
/**
* Sets the mode mappings.
*
@ -578,9 +640,11 @@ abstract class cache_administration_helper extends cache_helper {
$default = array();
$instance = cache_config::instance();
$stores = $instance->get_all_stores();
$locks = $instance->get_locks();
foreach ($stores as $name => $details) {
$class = $details['class'];
$store = new $class($details['name'], $details['configuration']);
$lock = (isset($details['lock'])) ? $locks[$details['lock']] : $instance->get_default_lock();
$record = array(
'name' => $name,
'plugin' => $details['plugin'],
@ -588,6 +652,7 @@ abstract class cache_administration_helper extends cache_helper {
'isready' => $store->is_ready(),
'requirementsmet' => $store->are_requirements_met(),
'mappings' => 0,
'lock' => $lock,
'modes' => array(
cache_store::MODE_APPLICATION =>
($store->get_supported_modes($return) & cache_store::MODE_APPLICATION) == cache_store::MODE_APPLICATION,
@ -870,6 +935,9 @@ abstract class cache_administration_helper extends cache_helper {
$url = new moodle_url('/cache/admin.php', array('action' => 'editstore', 'plugin' => $plugin, 'store' => $store));
$editform = new $class($url, array('plugin' => $plugin, 'store' => $store, 'locks' => $locks));
if (isset($stores[$store]['lock'])) {
$editform->set_data(array('lock' => $lock));
}
// See if the cachestore is going to want to load data for the form.
// If it has a customised add instance form then it is going to want to.
$storeclass = 'cachestore_'.$plugin;
@ -1030,10 +1098,86 @@ abstract class cache_administration_helper extends cache_helper {
$lockdata = array(
'name' => $name,
'default' => $default,
'uses' => $uses
'uses' => $uses,
'type' => get_string('pluginname', $lock['type'])
);
$locks[] = $lockdata;
$locks[$lock['name']] = $lockdata;
}
return $locks;
}
/**
* Returns an array of lock plugins for which we can add an instance.
*
* Suitable for use within an mform select element.
*
* @return array
*/
public static function get_addable_lock_options() {
$plugins = get_plugin_list_with_class('cachelock', '', 'lib.php');
$options = array();
$len = strlen('cachelock_');
foreach ($plugins as $plugin => $class) {
$method = "$class::can_add_instance";
if (is_callable($method) && !call_user_func($method)) {
// Can't add an instance of this plugin.
continue;
}
$options[substr($plugin, $len)] = get_string('pluginname', $plugin);
}
return $options;
}
/**
* Gets the form to use when adding a lock instance.
*
* @param string $plugin
* @param array $lockplugin
* @return cache_lock_form
* @throws coding_exception
*/
public static function get_add_lock_form($plugin, array $lockplugin = null) {
global $CFG; // Needed for includes.
$plugins = get_plugin_list('cachelock');
if (!array_key_exists($plugin, $plugins)) {
throw new coding_exception('Invalid cache lock plugin requested when trying to create a form.');
}
$plugindir = $plugins[$plugin];
$class = 'cache_lock_form';
if (file_exists($plugindir.'/addinstanceform.php') && in_array('cache_is_configurable', class_implements($class))) {
require_once($plugindir.'/addinstanceform.php');
if (class_exists('cachelock_'.$plugin.'_addinstance_form')) {
$class = 'cachelock_'.$plugin.'_addinstance_form';
if (!array_key_exists('cache_lock_form', class_parents($class))) {
throw new coding_exception('Cache lock plugin add instance forms must extend cache_lock_form');
}
}
}
return new $class(null, array('lock' => $plugin));
}
/**
* Gets configuration data from a new lock instance form.
*
* @param string $plugin
* @param stdClass $data
* @return array
* @throws coding_exception
*/
public static function get_lock_configuration_from_data($plugin, $data) {
global $CFG;
$file = $CFG->dirroot.'/cache/locks/'.$plugin.'/lib.php';
if (!file_exists($file)) {
throw new coding_exception('Invalid cache plugin provided. '.$file);
}
require_once($file);
$class = 'cachelock_'.$plugin;
if (!class_exists($class)) {
throw new coding_exception('Invalid cache plugin provided.');
}
if (array_key_exists('cache_is_configurable', class_implements($class))) {
return $class::config_get_configuration_array($data);
}
return array();
}
}

29
cache/renderer.php vendored
View File

@ -53,6 +53,7 @@ class core_cache_renderer extends plugin_renderer_base {
get_string('mappings', 'cache'),
get_string('modes', 'cache'),
get_string('supports', 'cache'),
get_string('lockingmeans', 'cache'),
get_string('actions', 'cache'),
);
$table->colclasses = array(
@ -62,6 +63,7 @@ class core_cache_renderer extends plugin_renderer_base {
'mappings',
'modes',
'supports',
'locking',
'actions'
);
$table->data = array();
@ -108,6 +110,11 @@ class core_cache_renderer extends plugin_renderer_base {
$readycell->attributes['class'] = 'store-requires-attention';
}
$lock = $store['lock']['name'];
if (!empty($store['lock']['default'])) {
$lock = get_string($store['lock']['name'], 'cache');
}
$row = new html_table_row(array(
$storename,
get_string('pluginname', 'cachestore_'.$store['plugin']),
@ -115,6 +122,7 @@ class core_cache_renderer extends plugin_renderer_base {
$store['mappings'],
join(', ', $modes),
join(', ', $supports),
$lock,
$info.join(', ', $htmlactions)
));
$row->attributes['class'] = 'store-'.$name;
@ -314,35 +322,50 @@ class core_cache_renderer extends plugin_renderer_base {
$table = new html_table();
$table->colclasses = array(
'name',
'type',
'default',
'uses',
// Useful later: 'actions'.
'actions'
);
$table->rowclasses = array(
'lock_name',
'lock_type',
'lock_default',
'lock_uses',
// Useful later: 'lock_actions',.
'lock_actions',
);
$table->head = array(
get_string('lockname', 'cache'),
get_string('locktype', 'cache'),
get_string('lockdefault', 'cache'),
get_string('lockuses', 'cache'),
// Useful later: get_string('actions', 'cache').
get_string('actions', 'cache')
);
$table->data = array();
$tick = $this->output->pix_icon('i/valid', '');
foreach ($locks as $lock) {
$actions = array();
if ($lock['uses'] === 0 && !$lock['default']) {
$url = new moodle_url('/cache/admin.php', array('lock' => $lock['name'], 'action' => 'deletelock', 'sesskey' => sesskey()));
$actions[] = html_writer::link($url, get_string('delete', 'cache'));
}
$table->data[] = new html_table_row(array(
new html_table_cell($lock['name']),
new html_table_cell($lock['type']),
new html_table_cell($lock['default'] ? $tick : ''),
new html_table_cell($lock['uses']),
new html_table_cell(join(' ', $actions))
));
}
$url = new moodle_url('/cache/admin.php', array('action' => 'newlockinstance', 'sesskey' => sesskey()));
$select = new single_select($url, 'lock', cache_administration_helper::get_addable_lock_options());
$select->label = get_string('addnewlockinstance', 'cache');
$html = html_writer::start_tag('div', array('id' => 'core-cache-lock-summary'));
$html .= $this->output->heading(get_string('locksummary', 'cache'), 3);
$html .= html_writer::table($table);
$html .= html_writer::tag('div', $this->output->render($select), array('class' => 'new-instance'));
$html .= html_writer::end_tag('div');
return $html;
}

View File

@ -28,6 +28,8 @@
$string['actions'] = 'Actions';
$string['addinstance'] = 'Add instance';
$string['addnewlockinstance'] = 'Add a new lock instance';
$string['addlocksuccess'] = 'Successfully added a new lock instance.';
$string['addstore'] = 'Add {$a} store';
$string['addstoresuccess'] = 'Successfully added a new {$a} store.';
$string['area'] = 'Area';
@ -59,6 +61,7 @@ $string['cachedef_yuimodules'] = 'YUI Module definitions';
$string['cachelock_file_default'] = 'Default file locking';
$string['cachestores'] = 'Cache stores';
$string['component'] = 'Component';
$string['confirmlockdeletion'] = 'Confirm lock deletion';
$string['confirmstoredeletion'] = 'Confirm store deletion';
$string['defaultmappings'] = 'Stores used when no mapping is present';
$string['defaultmappings_help'] = 'These are the default stores that will be used if you don\'t map one or more stores to the cache definition.';
@ -69,6 +72,10 @@ $string['default_session'] = 'Default session store';
$string['definition'] = 'Definition';
$string['definitionsummaries'] = 'Known cache definitions';
$string['delete'] = 'Delete';
$string['deletelock'] = 'Delete lock';
$string['deletelockconfirmation'] = 'Are you sure you want to delete the {$a} lock?';
$string['deletelockhasuses'] = 'You cannot delete this lock instance because it is being used by one or more stores.';
$string['deletelocksuccess'] = 'Successfully deleted the lock.';
$string['deletestore'] = 'Delete store';
$string['deletestoreconfirmation'] = 'Are you sure you want to delete the "{$a}" store?';
$string['deletestorehasmappings'] = 'You cannot delete this store because it has mappings. Please delete all mappings before deleting the store';
@ -83,13 +90,18 @@ $string['ex_unabletolock'] = 'Unable to acquire a lock for caching.';
$string['ex_unmetstorerequirements'] = 'You are unable to use this store at the present time. Please refer to the documentation to determine its requirements.';
$string['gethit'] = 'Get - Hit';
$string['getmiss'] = 'Get - Miss';
$string['invalidlock'] = 'Invalid lock';
$string['invalidplugin'] = 'Invalid plugin';
$string['invalidstore'] = 'Invalid cache store provided';
$string['lockdefault'] = 'Default';
$string['lockingmeans'] = 'Locking mechanism';
$string['lockmethod'] = 'Lock method';
$string['lockmethod_help'] = 'This is the method used for locking when required of this store.';
$string['lockname'] = 'Name';
$string['locknamedesc'] = 'The name must be unique and can on consist of the characters: a-zA-Z_';
$string['locknamenotunique'] = 'The name you have selected is not unique. Please select a unique name.';
$string['locksummary'] = 'Summary of cache lock instances.';
$string['locktype'] = 'Type';
$string['lockuses'] = 'Uses';
$string['mappings'] = 'Store mappings';
$string['mappingdefault'] = '(default)';

View File

@ -334,7 +334,8 @@
#core-cache-mode-mappings table {margin:0 auto;}
#core-cache-store-summaries .default-store td {color:#333;font-style: italic;}
#core-cache-rescan-definitions,
#core-cache-mode-mappings .edit-link {margin-top:0.5em;text-align:center;}
#core-cache-mode-mappings .edit-link,
#core-cache-lock-summary .new-instance {margin-top:0.5em;text-align:center;}
#core-cache-store-summaries .store-requires-attention {background-color:#ffd3d9;}
.tinymcesubplugins img.icon { padding-top: 0; padding-bottom: 0; }

View File

@ -722,7 +722,8 @@ img.iconsmall {
}
#core-cache-rescan-definitions,
#core-cache-mode-mappings .edit-link {
#core-cache-mode-mappings .edit-link,
#core-cache-lock-summary .new-instance {
margin-top: 0.5em;
text-align: center;
}

File diff suppressed because one or more lines are too long