diff --git a/core/includes/config.inc b/core/includes/config.inc index 51476b7..f0230c3 100644 --- a/core/includes/config.inc +++ b/core/includes/config.inc @@ -91,13 +91,13 @@ function config_sync_get_changes(StorageInterface $source_storage, StorageInterf foreach (array_intersect($source_names, $target_names) as $name) { $source_config_data = $source_storage->read($name); $target_config_data = $target_storage->read($name); - if ($source_config_data != $target_config_data) { + if ($source_config_data !== $target_config_data) { $config_changes['change'][] = $name; } } // Do not trigger subsequent synchronization operations if there are no - // changes in either category. + // changes in any category. if (empty($config_changes['create']) && empty($config_changes['change']) && empty($config_changes['delete'])) { return FALSE; } @@ -129,6 +129,48 @@ function config_sync_changes(array $config_changes, StorageInterface $source_sto } /** + * Imports configuration from FileStorage to DatabaseStorage. + * + * @return bool|null + * TRUE if configuration was imported successfully, FALSE in case of a + * synchronization error, or NULL if there are no changes to synchronize. + */ +function config_import() { + // Retrieve a list of differences between FileStorage and DatabaseStorage. + // @todo Leverage DI + config.storage.info. + $source_storage = new FileStorage(); + $target_storage = new DatabaseStorage(); + + $config_changes = config_sync_get_changes($source_storage, $target_storage); + if (empty($config_changes)) { + return; + } + + if (!lock_acquire(__FUNCTION__)) { + // Another request is synchronizing configuration. + // Return a negative result for UI purposes. We do not differentiate between + // an actual synchronization error and a failed lock, because concurrent + // synchronizations are an edge-case happening only when multiple developers + // or site builders attempt to do it without coordinating. + return FALSE; + } + + $success = TRUE; + try { + $remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage); + config_sync_changes($remaining_changes, $source_storage, $target_storage); + // Flush all caches and reset static variables after a successful import. + drupal_flush_all_caches(); + } + catch (ConfigException $e) { + watchdog_exception('config_import', $e); + $success = FALSE; + } + lock_release(__FUNCTION__); + return $success; +} + +/** * Invokes MODULE_config_import() callbacks for configuration changes. * * @param array $config_changes @@ -159,8 +201,18 @@ function config_import_invoke_owner(array $config_changes, StorageInterface $sou $old_config ->setName($name) ->load(); + // If no data exists in the target storage for the delete operation, + // then this change was manually specified and can be skipped. + if ($op == 'delete' && $old_config->isNew()) { + continue; + } $data = $source_storage->read($name); + // If no data exists in the source storage for the create operation, + // then this change was manually specified and can be skipped. + if ($op == 'create' && $data === FALSE) { + continue; + } $new_config = new Config($storage_dispatcher); $new_config->setName($name); if ($data !== FALSE) { @@ -176,3 +228,20 @@ function config_import_invoke_owner(array $config_changes, StorageInterface $sou } return $config_changes; } + +/** + * Exports configuration from DatabaseStorage to FileStorage. + */ +function config_export() { + // Retrieve a list of differences between DatabaseStorage and FileStorage. + // @todo Leverage DI + config.storage.info. + $source_storage = new DatabaseStorage(); + $target_storage = new FileStorage(); + + $config_changes = config_sync_get_changes($source_storage, $target_storage); + if (empty($config_changes)) { + return; + } + config_sync_changes($config_changes, $source_storage, $target_storage); + return TRUE; +} diff --git a/core/includes/form.inc b/core/includes/form.inc index 395cca7..f5acd24 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -3534,6 +3534,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/ConfigThingie/ConfigThingieBase.php b/core/lib/Drupal/Core/ConfigThingie/ConfigThingieBase.php new file mode 100644 index 0000000..3f83cf4 --- /dev/null +++ b/core/lib/Drupal/Core/ConfigThingie/ConfigThingieBase.php @@ -0,0 +1,232 @@ +setConfig($config); + } + + /** + * Returns the configuration thingie object's name prefix. + * + * @return string + * The configuration object name prefix; e.g., for a 'node.type.article' + * thingie, this returns 'node.type'. + */ + abstract public function getConfigPrefix(); + + /** + * Returns the fully qualified configuration object name for this configuration thingie. + * + * @return string + * The configuration object name for this configuration thingie. Normally, + * ConfigThingieInterface::getConfigPrefix() and + * ConfigThingieInterface::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 configuration thingie. + * + * @return string + * The basename for events/hooks; e.g., for a 'node.type.article' thingie, + * this returns 'node_type'. Normally, this is auto-generated based on + * ConfigThingieInterface::getConfigPrefix(). + */ + public function getEventBasename() { + return str_replace('.', '_', $this->getConfigPrefix()); + } + + /** + * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::getID(). + */ + public function getID() { + return $this->config->get($this->idKey); + } + + /** + * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::isNew(). + */ + public function isNew() { + return $this->config->isNew(); + } + + /** + * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::getLabel(). + */ + public function getLabel() { + return $this->config->get($this->labelKey); + } + + /** + * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::get(). + */ + public function get($property_name) { + return $this->config->get($property_name); + } + + /** + * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::set(). + */ + public function set($property_name, $value) { + return $this->config->set($property_name, $value); + } + + /** + * Sets the configuration object for this configuration thingie. + * + * @param Drupal\Core\Config\Config $config + * The configuration object containing the data for this configuration + * thingie. + * + * @return Drupal\Core\ConfigThingie\ConfigThingieInterface + * This configuration thingie (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 configuration thingie. + * + * @param Drupal\Core\Config\Config $config + * The configuration object containing the original data for this + * configuration thingie. + * + * @return Drupal\Core\ConfigThingie\ConfigThingieInterface + * This configuration thingie (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\ConfigThingie\ConfigThingieInterface::save(). + */ + public function save() { + // Provide the original configuration thingie in $this->original, if any. + // originalID is only set, if this configuration thingie already existed + // prior to saving. + if (isset($this->originalID)) { + // Load the original configuration object. + // This cannot use ConfigThingieBase::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\ConfigThingie\ConfigThingieInterface::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/ConfigThingie/ConfigThingieInterface.php b/core/lib/Drupal/Core/ConfigThingie/ConfigThingieInterface.php new file mode 100644 index 0000000..506ffa2 --- /dev/null +++ b/core/lib/Drupal/Core/ConfigThingie/ConfigThingieInterface.php @@ -0,0 +1,91 @@ + t('There are no configuration changes.'), + ); + return $form; + } + + foreach ($config_changes as $config_change_type => $config_files) { + if (empty($config_files)) { + continue; + } + $form[$config_change_type] = array( + '#type' => 'fieldset', + '#title' => $config_change_type . ' (' . count($config_files) . ')', + '#collapsible' => TRUE, + ); + $form[$config_change_type]['config_files'] = array( + '#theme' => 'table', + '#header' => array('Name'), + ); + foreach ($config_files as $config_file) { + $form[$config_change_type]['config_files']['#rows'][] = array($config_file); + } + } + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Import'), + ); + return $form; +} + +/** + * Form submission handler for config_admin_import_form(). + */ +function config_admin_import_form_submit($form, &$form_state) { + if (config_import()) { + drupal_set_message(t('The configuration was imported successfully.')); + } + else { + // Another request may be synchronizing configuration already. Wait for it + // to complete before returning the error, so already synchronized changes + // do not appear again. + lock_wait(__FUNCTION__); + drupal_set_message(t('The import failed due to an error. Any errors have been logged.'), 'error'); + } +} + diff --git a/core/modules/config/config.module b/core/modules/config/config.module index b3d9bbc..3d4fcfe 100644 --- a/core/modules/config/config.module +++ b/core/modules/config/config.module @@ -1 +1,33 @@ t('Import configuration'), + 'restrict access' => TRUE, + ); + return $permissions; +} + +/** + * Implements hook_menu(). + */ +function config_menu() { + $items['admin/config/development/import'] = array( + 'title' => 'Import configuration', + 'description' => 'Import and synchronize configuration changes.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('config_admin_import_form'), + 'access arguments' => array('import configuration'), + 'file' => 'config.admin.inc', + ); + return $items; +} + diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigConfigurableTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigConfigurableTest.php new file mode 100644 index 0000000..cfb4bcc --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigConfigurableTest.php @@ -0,0 +1,80 @@ + '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 thingie. + $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 thingie. + $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 thingie. + $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 thingie. + $edit = array( + 'id' => $id, + 'label' => 'Thingie', + ); + $this->drupalPost('admin/structure/config_test/add', $edit, 'Save'); + $this->assertResponse(200); + $this->assertText('Thingie'); + + // Rename the thingie'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/ConfigImportTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php new file mode 100644 index 0000000..08eb242 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php @@ -0,0 +1,185 @@ + 'Import configuration', + 'description' => 'Tests importing configuration from files into active store.', + 'group' => 'Configuration', + ); + } + + function setUp() { + parent::setUp(array('config_test')); + + // Clear out any possibly existing hook invocation records. + unset($GLOBALS['hook_config_test_dynamic']); + } + + /** + * Tests omission of module APIs for bare configuration operations. + */ + function testNoImport() { + $dynamic_name = 'config_test.dynamic.default'; + + // Verify the default configuration values exist. + $config = config($dynamic_name); + $this->assertIdentical($config->get('id'), 'default'); + + // Verify that a bare config() does not involve module APIs. + $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic'])); + + // Export. + config_export(); + + // Verify that config_export() does not involve module APIs. + $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic'])); + } + + /** + * Tests deletion of configuration during import. + */ + function testDeleted() { + $name = 'config_test.system'; + $dynamic_name = 'config_test.dynamic.default'; + + // Verify the default configuration values exist. + $config = config($name); + $this->assertIdentical($config->get('foo'), 'bar'); + $config = config($dynamic_name); + $this->assertIdentical($config->get('id'), 'default'); + + // Export. + config_export(); + + // Delete the configuration objects. + $file_storage = new FileStorage(); + $file_storage->delete($name); + $file_storage->delete($dynamic_name); + + // Import. + config_import(); + + // Verify the values have disappeared. + $database_storage = new DatabaseStorage(); + $this->assertIdentical($database_storage->read($name), FALSE); + $this->assertIdentical($database_storage->read($dynamic_name), FALSE); + + $config = config($name); + $this->assertIdentical($config->get('foo'), NULL); + $config = config($dynamic_name); + $this->assertIdentical($config->get('id'), NULL); + + // Verify that appropriate module API hooks have been invoked. + $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['load'])); + $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['presave'])); + $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['insert'])); + $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['update'])); + $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['predelete'])); + $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['delete'])); + } + + /** + * Tests creation of configuration during import. + */ + function testNew() { + $name = 'config_test.new'; + $dynamic_name = 'config_test.dynamic.new'; + + // Verify the configuration to create does not exist yet. + $file_storage = new FileStorage(); + $this->assertIdentical($file_storage->exists($name), FALSE, $name . ' not found.'); + $this->assertIdentical($file_storage->exists($dynamic_name), FALSE, $dynamic_name . ' not found.'); + + // Export. + config_export(); + + // Create new configuration objects. + $file_storage->write($name, array( + 'add_me' => 'new value', + )); + $file_storage->write($dynamic_name, array( + 'id' => 'new', + 'label' => 'New', + )); + $this->assertIdentical($file_storage->exists($name), TRUE, $name . ' found.'); + $this->assertIdentical($file_storage->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); + + // Import. + config_import(); + + // Verify the values appeared. + $config = config($name); + $this->assertIdentical($config->get('add_me'), 'new value'); + $config = config($dynamic_name); + $this->assertIdentical($config->get('label'), 'New'); + + // Verify that appropriate module API hooks have been invoked. + $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'])); + } + + /** + * Tests updating of configuration during import. + */ + function testUpdated() { + $name = 'config_test.system'; + $dynamic_name = 'config_test.dynamic.default'; + + // Export. + config_export(); + + // Replace the file content of the existing configuration objects. + $file_storage = new FileStorage(); + $this->assertIdentical($file_storage->exists($name), TRUE, $name . ' found.'); + $this->assertIdentical($file_storage->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); + $file_storage->write($name, array( + 'foo' => 'beer', + )); + $file_storage->write($dynamic_name, array( + 'id' => 'default', + 'label' => 'Updated', + )); + + // Verify the active store still returns the default values. + $config = config($name); + $this->assertIdentical($config->get('foo'), 'bar'); + $config = config($dynamic_name); + $this->assertIdentical($config->get('label'), 'Default'); + + // Import. + config_import(); + + // Verify the values were updated. + $config = config($name); + $this->assertIdentical($config->get('foo'), 'beer'); + $config = config($dynamic_name); + $this->assertIdentical($config->get('label'), 'Updated'); + + // Verify that appropriate module API hooks have been invoked. + $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['load'])); + $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['presave'])); + $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['insert'])); + $this->assertTrue(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/lib/Drupal/config/Tests/ConfigInstallTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php index 7ec6d8e..a9676ef 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php @@ -46,5 +46,14 @@ class ConfigInstallTest extends WebTestBase { // Verify that configuration import callback was invoked for the dynamic // thingie. $this->assertTrue($GLOBALS['hook_config_import']); + + // Verify that config_test API hooks were invoked for the dynamic default + // thingie. + $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/config_test.delete.yml b/core/modules/config/tests/config_test/config/config_test.delete.yml new file mode 100644 index 0000000..b8ccb67 --- /dev/null +++ b/core/modules/config/tests/config_test/config/config_test.delete.yml @@ -0,0 +1 @@ +delete_me: bar 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..2ec6831 --- /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; } @@ -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; } @@ -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..4efaae5 --- /dev/null +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php @@ -0,0 +1,36 @@ +getID(); + } +}