diff --git a/core/includes/form.inc b/core/includes/form.inc index eee8be1..ff500fa 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -3536,6 +3536,7 @@ function form_process_machine_name($element, &$form_state) { // 'source' only) would leave all other properties undefined, if the defaults // were defined in hook_element_info(). Therefore, we apply the defaults here. $element['#machine_name'] += array( + // @todo Use 'label' by default. 'source' => array('name'), 'target' => '#' . $element['#id'], 'label' => t('Machine name'), diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php index a749a4b..ea14595 100644 --- a/core/lib/Drupal/Core/Config/Config.php +++ b/core/lib/Drupal/Core/Config/Config.php @@ -268,6 +268,7 @@ class Config { * Deletes the configuration object. */ public function delete() { + // @todo Consider to remove the pruning of data for Config::delete(). $this->data = array(); $this->storageDispatcher->selectStorage('write', $this->name)->delete($this->name); $this->isNew = TRUE; diff --git a/core/lib/Drupal/Core/Configurable/ConfigurableBase.php b/core/lib/Drupal/Core/Configurable/ConfigurableBase.php new file mode 100644 index 0000000..4f22661 --- /dev/null +++ b/core/lib/Drupal/Core/Configurable/ConfigurableBase.php @@ -0,0 +1,232 @@ +setConfig($config); + } + + /** + * Returns the configurable object's name prefix. + * + * @return string + * The configuration object name prefix; e.g., for a 'node.type.article' + * configurable object, this returns 'node.type'. + */ + abstract public function getConfigPrefix(); + + /** + * Returns the fully qualified configuration object name for this configurable object. + * + * @return string + * The configuration object name for this configurable object. Normally, + * ConfigurableInterface::getConfigPrefix() and + * ConfigurableInterface::getID() form the full configuration object name; + * i.e., '[prefix].[id]'. + */ + public function getConfigName() { + return $this->getConfigPrefix() . '.' . $this->getID(); + } + + /** + * Returns the basename used for events/hooks for this configurable object. + * + * @return string + * The basename for events/hooks; e.g., for a 'node.type.article' + * configurable object, this returns 'node_type'. Normally, this is + * auto-generated based on ConfigurableInterface::getConfigPrefix(). + */ + public function getEventBasename() { + return str_replace('.', '_', $this->getConfigPrefix()); + } + + /** + * Implements Drupal\Core\Configurable\ConfigurableInterface::getID(). + */ + public function getID() { + return $this->config->get($this->idKey); + } + + /** + * Implements Drupal\Core\Configurable\ConfigurableInterface::isNew(). + */ + public function isNew() { + return $this->config->isNew(); + } + + /** + * Implements Drupal\Core\Configurable\ConfigurableInterface::getLabel(). + */ + public function getLabel() { + return $this->config->get($this->labelKey); + } + + /** + * Implements Drupal\Core\Configurable\ConfigurableInterface::get(). + */ + public function get($property_name) { + return $this->config->get($property_name); + } + + /** + * Implements Drupal\Core\Configurable\ConfigurableInterface::set(). + */ + public function set($property_name, $value) { + return $this->config->set($property_name, $value); + } + + /** + * Sets the configuration object for this configurable object. + * + * @param Drupal\Core\Config\Config $config + * The configuration object containing the data for this configurable + * object. + * + * @return Drupal\Core\Configurable\ConfigurableInterface + * This configurable object (suitable for chained method calls). + */ + public function setConfig(Config $config) { + $this->config = $config; + // Only set originalID, if the passed in configuration object is stored + // already. + if (!$this->config->isNew()) { + $this->originalID = $this->getID(); + } + + // Allow modules to react upon load. + module_invoke_all($this->getEventBasename() . '_load', $this); + + return $this; + } + + /** + * Sets the unchanged original of this configurable object. + * + * @param Drupal\Core\Config\Config $config + * The configuration object containing the original data for this + * configurable object. + * + * @return Drupal\Core\Configurable\ConfigurableInterface + * This configurable object (suitable for chained method calls). + */ + public function setOriginal(Config $config) { + $this->original = new $this($config); + // Ensure that originalID contains the ID of the supplied original + // configuration object. setOriginal() may be called from outside of this + // class (e.g., hook_config_import()) in order to set a specific original. + $this->originalID = $this->original->getID(); + return $this; + } + + /** + * Implements Drupal\Core\Configurable\ConfigurableInterface::save(). + */ + public function save() { + // Provide the original configurable object in $this->original, if any. + // originalID is only set, if this configurable object already existed + // prior to saving. + if (isset($this->originalID)) { + // Load the original configuration object. + // This cannot use ConfigurableBase::getConfigName(), since that would + // yield the name using the current/new ID. + $original_config = config($this->getConfigPrefix() . '.' . $this->originalID); + // Given the original configuration object, instantiate a new class of the + // current class, and provide it in $this->original. + $this->setOriginal($original_config); + } + + // Ensure that the configuration object name uses the current ID. + $this->config->setName($this->getConfigName()); + + // Allow modules to react prior to saving. + module_invoke_all($this->getEventBasename() . '_presave', $this); + + // Save the configuration object. + $this->config->save(); + + if (isset($this->originalID)) { + // Allow modules to react after inserting new configuration. + module_invoke_all($this->getEventBasename() . '_update', $this); + + // Delete the original configuration, if it was renamed. + if ($this->originalID !== $this->getID()) { + // Configuration data is emptied out upon delete, so back it up and + // re-inject it. Delete the old configuration data directly; hooks will + // get and will be able to react to the data in $this->original. + // @todo Consider to remove the pruning of data for Config::delete(). + $original_data = $original_config->get(); + $original_config->delete(); + $original_config->setData($original_data); + } + } + else { + // Allow modules to react after updating existing configuration. + module_invoke_all($this->getEventBasename() . '_insert', $this); + } + + return $this; + } + + /** + * Implements Drupal\Core\Configurable\ConfigurableInterface::delete(). + */ + public function delete() { + // Allow modules to react prior to deleting the configuration. + module_invoke_all($this->getEventBasename() . '_predelete', $this); + + // Delete the configuration object. + $this->config->delete(); + + // Allow modules to react after deleting the configuration. + module_invoke_all($this->getEventBasename() . '_delete', $this); + } +} diff --git a/core/lib/Drupal/Core/Configurable/ConfigurableInterface.php b/core/lib/Drupal/Core/Configurable/ConfigurableInterface.php new file mode 100644 index 0000000..921df12 --- /dev/null +++ b/core/lib/Drupal/Core/Configurable/ConfigurableInterface.php @@ -0,0 +1,91 @@ + 'Configurable configuration', + 'description' => 'Tests configurable configuration.', + 'group' => 'Configuration', + ); + } + + function setUp() { + parent::setUp(array('config_test')); + } + + /** + * Tests basic CRUD operations through the UI. + */ + function testCRUD() { + // Create a configurable object. + $id = 'thingie'; + $edit = array( + 'id' => $id, + 'label' => 'Thingie', + ); + $this->drupalPost('admin/structure/config_test/add', $edit, 'Save'); + $this->assertResponse(200); + $this->assertText('Thingie'); + + // Update the configurable object. + $this->assertLinkByHref('admin/structure/config_test/manage/' . $id); + $edit = array( + 'label' => 'Thongie', + ); + $this->drupalPost('admin/structure/config_test/manage/' . $id, $edit, 'Save'); + $this->assertResponse(200); + $this->assertNoText('Thingie'); + $this->assertText('Thongie'); + + // Delete the configurable object. + $this->assertLinkByHref('admin/structure/config_test/manage/' . $id . '/delete'); + $this->drupalPost('admin/structure/config_test/manage/' . $id . '/delete', array(), 'Delete'); + $this->assertResponse(200); + $this->assertNoText('Thingie'); + $this->assertNoText('Thongie'); + + // Re-create a configurable object. + $edit = array( + 'id' => $id, + 'label' => 'Thingie', + ); + $this->drupalPost('admin/structure/config_test/add', $edit, 'Save'); + $this->assertResponse(200); + $this->assertText('Thingie'); + + // Rename the configurable object's ID/machine name. + $this->assertLinkByHref('admin/structure/config_test/manage/' . $id); + $new_id = 'zingie'; + $edit = array( + 'id' => $new_id, + 'label' => 'Zingie', + ); + $this->drupalPost('admin/structure/config_test/manage/' . $id, $edit, 'Save'); + $this->assertResponse(200); + $this->assertNoText('Thingie'); + $this->assertText('Zingie'); + } +} diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php index 7ec6d8e..26dacea 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php @@ -26,12 +26,12 @@ class ConfigInstallTest extends WebTestBase { */ function testModuleInstallation() { $default_config = 'config_test.system'; - $default_thingie = 'config_test.dynamic.default'; + $default_configurable = 'config_test.dynamic.default'; // Verify that default module config does not exist before installation yet. $config = config($default_config); $this->assertIdentical($config->isNew(), TRUE); - $config = config($default_thingie); + $config = config($default_configurable); $this->assertIdentical($config->isNew(), TRUE); // Install the test module. @@ -40,11 +40,20 @@ class ConfigInstallTest extends WebTestBase { // Verify that default module config exists. $config = config($default_config); $this->assertIdentical($config->isNew(), FALSE); - $config = config($default_thingie); + $config = config($default_configurable); $this->assertIdentical($config->isNew(), FALSE); // Verify that configuration import callback was invoked for the dynamic - // thingie. + // configurable object. $this->assertTrue($GLOBALS['hook_config_import']); + + // Verify that config_test API hooks were invoked for the dynamic default + // configurable object. + $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['load'])); + $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['presave'])); + $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['insert'])); + $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['update'])); + $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['predelete'])); + $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['delete'])); } } diff --git a/core/modules/config/tests/config_test/config_test.hooks.inc b/core/modules/config/tests/config_test/config_test.hooks.inc new file mode 100644 index 0000000..e794d7e --- /dev/null +++ b/core/modules/config/tests/config_test/config_test.hooks.inc @@ -0,0 +1,52 @@ +save(); + $config_test = new ConfigTest($new_config); + $config_test->save(); return TRUE; } @@ -20,7 +25,7 @@ function config_test_config_import_create($name, $new_config, $old_config) { * Implements MODULE_config_import_change(). */ function config_test_config_import_change($name, $new_config, $old_config) { - // Only configurable thingies require custom handling. Any other module + // Only configurable objects require custom handling. Any other module // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; @@ -28,7 +33,9 @@ function config_test_config_import_change($name, $new_config, $old_config) { // Set a global value we can check in test code. $GLOBALS['hook_config_import'] = __FUNCTION__; - $new_config->save(); + $config_test = new ConfigTest($new_config); + $config_test->setOriginal($old_config); + $config_test->save(); return TRUE; } @@ -36,7 +43,7 @@ function config_test_config_import_change($name, $new_config, $old_config) { * Implements MODULE_config_import_delete(). */ function config_test_config_import_delete($name, $new_config, $old_config) { - // Only configurable thingies require custom handling. Any other module + // Only configurable objects require custom handling. Any other module // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; @@ -44,7 +51,208 @@ function config_test_config_import_delete($name, $new_config, $old_config) { // Set a global value we can check in test code. $GLOBALS['hook_config_import'] = __FUNCTION__; - $old_config->delete(); + $config_test = new ConfigTest($old_config); + $config_test->delete(); return TRUE; } +/** + * Implements hook_menu(). + */ +function config_test_menu() { + $items['admin/structure/config_test'] = array( + 'title' => 'Test configuration', + 'page callback' => 'config_test_list_page', + 'access callback' => TRUE, + ); + $items['admin/structure/config_test/add'] = array( + 'title' => 'Add test configuration', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('config_test_form'), + 'access callback' => TRUE, + 'type' => MENU_LOCAL_ACTION, + ); + $items['admin/structure/config_test/manage/%config_test'] = array( + 'title' => 'Edit test configuration', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('config_test_form', 4), + 'access callback' => TRUE, + ); + $items['admin/structure/config_test/manage/%config_test/edit'] = array( + 'title' => 'Edit', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + $items['admin/structure/config_test/manage/%config_test/delete'] = array( + 'title' => 'Delete', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('config_test_delete_form', 4), + 'access callback' => TRUE, + 'type' => MENU_LOCAL_TASK, + ); + return $items; +} + +/** + * Loads a ConfigTest object. + * + * @param string $id + * The ID of the ConfigTest object to load. + */ +function config_test_load($id) { + $config = config('config_test.dynamic.' . $id); + if ($config->isNew()) { + return FALSE; + } + return new ConfigTest($config); +} + +/** + * Saves a ConfigTest object. + * + * @param Drupal\config_test\ConfigTest $config_test + * The ConfigTest object to save. + */ +function config_test_save(ConfigTest $config_test) { + return $config_test->save(); +} + +/** + * Deletes a ConfigTest object. + * + * @param string $id + * The ID of the ConfigTest object to delete. + */ +function config_test_delete($id) { + $config = config('config_test.dynamic.' . $id); + $config_test = new ConfigTest($config); + return $config_test->delete(); +} + +/** + * Page callback; Lists available ConfigTest objects. + */ +function config_test_list_page() { + $config_names = config_get_storage_names_with_prefix('config_test.dynamic.'); + $rows = array(); + foreach ($config_names as $config_name) { + $config_test = new ConfigTest(config($config_name)); + $row = array(); + $row['name']['data'] = array( + '#type' => 'link', + '#title' => $config_test->getLabel(), + '#href' => $config_test->getUri(), + ); + $row['delete']['data'] = array( + '#type' => 'link', + '#title' => t('Delete'), + '#href' => $config_test->getUri() . '/delete', + ); + $rows[] = $row; + } + $build = array( + '#theme' => 'table', + '#header' => array('Name', 'Operations'), + '#rows' => $rows, + '#empty' => format_string('No test configuration defined. Add some', array( + '@add-url' => url('admin/structure/config_test/add'), + )), + ); + return $build; +} + +/** + * Form constructor to add or edit a ConfigTest object. + * + * @param Drupal\config_test\ConfigTest $config_test + * (optional) An existing ConfigTest object to edit. If omitted, the form + * creates a new ConfigTest. + */ +function config_test_form($form, &$form_state, ConfigTest $config_test = NULL) { + if (!isset($config_test)) { + $config_test = new ConfigTest(config(NULL)); + } + $form_state['config_test'] = $config_test; + + $form['label'] = array( + '#type' => 'textfield', + '#title' => 'Label', + '#default_value' => $config_test->getLabel(), + '#required' => TRUE, + ); + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $config_test->getID(), + '#required' => TRUE, + '#machine_name' => array( + 'exists' => 'config_test_load', + // @todo Update form_process_machine_name() to use 'label' by default. + 'source' => array('label'), + ), + ); + $form['style'] = array( + '#type' => 'select', + '#title' => 'Image style', + '#options' => array(), + '#default_value' => $config_test->get('style'), + '#access' => FALSE, + ); + if (module_exists('image')) { + $form['style']['#access'] = TRUE; + $form['style']['#options'] = image_style_options(); + } + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => 'Save'); + + return $form; +} + +/** + * Form submission handler for config_test_form(). + */ +function config_test_form_submit($form, &$form_state) { + form_state_values_clean($form_state); + + $config_test = $form_state['config_test']; + + foreach ($form_state['values'] as $key => $value) { + $config_test->set($key, $value); + } + $config_test->save(); + + if (!empty($config_test->original)) { + drupal_set_message(format_string('%label configuration has been updated.', array('%label' => $config_test->getLabel()))); + } + else { + drupal_set_message(format_string('%label configuration has been created.', array('%label' => $config_test->getLabel()))); + } + + $form_state['redirect'] = 'admin/structure/config_test'; +} + +/** + * Form constructor to delete a ConfigTest object. + * + * @param Drupal\config_test\ConfigTest $config_test + * The ConfigTest object to delete. + */ +function config_test_delete_form($form, &$form_state, ConfigTest $config_test) { + $form_state['config_test'] = $config_test; + + $form['id'] = array('#type' => 'value', '#value' => $config_test->getID()); + return confirm_form($form, + format_string('Are you sure you want to delete %label', array('%label' => $config_test->getLabel())), + 'admin/structure/config_test', + NULL, + 'Delete' + ); +} + +/** + * Form submission handler for config_test_delete_form(). + */ +function config_test_delete_form_submit($form, &$form_state) { + $form_state['config_test']->delete(); + $form_state['redirect'] = 'admin/structure/config_test'; +} diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php new file mode 100644 index 0000000..748e81f --- /dev/null +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php @@ -0,0 +1,36 @@ +getID(); + } +}