diff --git a/core/includes/form.inc b/core/includes/form.inc index 7498ea5..f162d85 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -3533,6 +3533,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 2f4d14a..c5423ca 100644 --- a/core/lib/Drupal/Core/Config/Config.php +++ b/core/lib/Drupal/Core/Config/Config.php @@ -270,6 +270,7 @@ public function sortByKey(array &$data) { * Deletes the configuration object. */ public function delete() { + // @todo Consider to remove the pruning of data for Config::delete(). $this->data = array(); $this->storage->delete($this->name); $this->isNew = TRUE; diff --git a/core/modules/config/config.api.php b/core/modules/config/config.api.php index f0c3afa..6aef002 100644 --- a/core/modules/config/config.api.php +++ b/core/modules/config/config.api.php @@ -31,12 +31,12 @@ * A configuration object containing the old configuration data. */ function MODULE_config_import_create($name, $new_config, $old_config) { - // Only configurable thingies require custom handling. Any other module + // Only configurable entities require custom handling. Any other module // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; } - $config_test = new ConfigTest($new_config); + $config_test = entity_create('config_test', $new_config->get()); $config_test->save(); return TRUE; } @@ -60,13 +60,25 @@ function MODULE_config_import_create($name, $new_config, $old_config) { * A configuration object containing the old configuration data. */ function MODULE_config_import_change($name, $new_config, $old_config) { - // Only configurable thingies require custom handling. Any other module + // Only configurable entities require custom handling. Any other module // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; } - $config_test = new ConfigTest($new_config); - $config_test->setOriginal($old_config); + + // @todo Make this less ugly. + $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1); + $config_test = entity_load('config_test', $id); + + $config_test->original = clone $config_test; + foreach ($old_config->get() as $property => $value) { + $config_test->original->$property = $value; + } + + foreach ($new_config->get() as $property => $value) { + $config_test->$property = $value; + } + $config_test->save(); return TRUE; } @@ -90,7 +102,7 @@ function MODULE_config_import_change($name, $new_config, $old_config) { * A configuration object containing the old configuration data. */ function MODULE_config_import_delete($name, $new_config, $old_config) { - // Only configurable thingies require custom handling. Any other module + // Only configurable entities require custom handling. Any other module // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; @@ -100,8 +112,8 @@ function MODULE_config_import_delete($name, $new_config, $old_config) { // But that is impossible currently, since the config system only knows // about deleted and added changes. Introduce an 'old_ID' key within // config objects as a standard? - $config_test = new ConfigTest($old_config); - $config_test->delete(); + $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1); + config_test_delete($id); return TRUE; } diff --git a/core/modules/config/lib/Drupal/config/ConfigStorageController.php b/core/modules/config/lib/Drupal/config/ConfigStorageController.php new file mode 100644 index 0000000..d41ef24 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/ConfigStorageController.php @@ -0,0 +1,333 @@ +entityType = $entityType; + $this->entityInfo = entity_get_info($entityType); + $this->hookLoadArguments = array(); + $this->idKey = $this->entityInfo['entity keys']['id']; + + // The UUID key and property is hard-coded for all configurables. + $this->uuidKey = 'uuid'; + } + + /** + * Implements Drupal\entity\EntityStorageControllerInterface::resetCache(). + */ + public function resetCache(array $ids = NULL) { + // The configuration system is fast enough and/or implements its own + // (advanced) caching mechanism already. + } + + /** + * Implements Drupal\entity\EntityStorageControllerInterface::load(). + */ + public function load($ids = array(), $conditions = array()) { + $entities = array(); + + // Create a new variable which is either a prepared version of the $ids + // array for later comparison with the entity cache, or FALSE if no $ids + // were passed. + $passed_ids = !empty($ids) ? array_flip($ids) : FALSE; + + // Load any remaining entities. This is the case if $ids + // is set to FALSE (so we load all entities), + // or if $conditions was passed without $ids. + if ($ids === FALSE || $ids || ($conditions && !$passed_ids)) { + $queried_entities = $this->buildQuery($ids, $conditions); + } + + // Pass all entities loaded from the database through $this->attachLoad(), + // which calls the + // entity type specific load callback, for example hook_node_type_load(). + if (!empty($queried_entities)) { + $this->attachLoad($queried_entities); + $entities += $queried_entities; + } + + // Ensure that the returned array is ordered the same as the original + // $ids array if this was passed in and remove any invalid ids. + if ($passed_ids) { + // Remove any invalid ids from the array. + $passed_ids = array_intersect_key($passed_ids, $entities); + foreach ($entities as $entity) { + $passed_ids[$entity->{$this->idKey}] = $entity; + } + $entities = $passed_ids; + } + + return $entities; + } + + /** + * Builds the query to load the entity. + * + * This has full revision support. For entities requiring special queries, + * the class can be extended, and the default query can be constructed by + * calling parent::buildQuery(). This is usually necessary when the object + * being loaded needs to be augmented with additional data from another + * table, such as loading node type into comments or vocabulary machine name + * into terms, however it can also support $conditions on different tables. + * See Drupal\comment\CommentStorageController::buildQuery() or + * Drupal\taxonomy\TermStorageController::buildQuery() for examples. + * + * @param $ids + * An array of entity IDs, or FALSE to load all entities. + * @param $conditions + * An array of conditions in the form 'field' => $value. + * @param $revision_id + * The ID of the revision to load, or FALSE if this query is asking for the + * most current revision(s). + * + * @return SelectQuery + * A SelectQuery object for loading the entity. + */ + protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { + $config_class = $this->entityInfo['entity class']; + $prefix = $config_class::getConfigPrefix() . '.'; + + // @todo Handle $conditions? + if ($ids === FALSE) { + $names = drupal_container()->get('config.storage')->listAll($prefix); + $result = array(); + foreach ($names as $name) { + $config = config($name); + $result[$config->get($this->idKey)] = new $config_class($config->get(), $this->entityType); + } + return $result; + } + else { + $result = array(); + foreach ($ids as $id) { + $config = config($prefix . $id); + if (!$config->isNew()) { + $result[$id] = new $config_class($config->get(), $this->entityType); + } + } + return $result; + } + } + + /** + * Attaches data to entities upon loading. + * + * This will attach fields, if the entity is fieldable. It calls + * hook_entity_load() for modules which need to add data to all entities. + * It also calls hook_TYPE_load() on the loaded entities. For example + * hook_node_load() or hook_user_load(). If your hook_TYPE_load() + * expects special parameters apart from the queried entities, you can set + * $this->hookLoadArguments prior to calling the method. + * See Drupal\node\NodeStorageController::attachLoad() for an example. + * + * @param $queried_entities + * Associative array of query results, keyed on the entity ID. + * @param $revision_id + * ID of the revision that was loaded, or FALSE if the most current revision + * was loaded. + */ + protected function attachLoad(&$queried_entities, $revision_id = FALSE) { + // Call hook_entity_load(). + foreach (module_implements('entity_load') as $module) { + $function = $module . '_entity_load'; + $function($queried_entities, $this->entityType); + } + // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are + // always the queried entities, followed by additional arguments set in + // $this->hookLoadArguments. + $args = array_merge(array($queried_entities), $this->hookLoadArguments); + foreach (module_implements($this->entityType . '_load') as $module) { + call_user_func_array($module . '_' . $this->entityType . '_load', $args); + } + } + + /** + * Implements Drupal\entity\EntityStorageControllerInterface::create(). + */ + public function create(array $values) { + $class = isset($this->entityInfo['entity class']) ? $this->entityInfo['entity class'] : 'Drupal\entity\Entity'; + + $entity = new $class($values, $this->entityType); + + // Assign a new UUID if there is none yet. + if (!isset($entity->{$this->uuidKey})) { + $uuid = new Uuid(); + $entity->{$this->uuidKey} = $uuid->generate(); + } + + return $entity; + } + + /** + * Implements Drupal\entity\EntityStorageControllerInterface::delete(). + */ + public function delete($ids) { + $entities = $ids ? $this->load($ids) : FALSE; + if (!$entities) { + // If no IDs or invalid IDs were passed, do nothing. + return; + } + + $this->preDelete($entities); + foreach ($entities as $id => $entity) { + $this->invokeHook('predelete', $entity); + } + + foreach ($entities as $id => $entity) { + $config = config($entity::getConfigPrefix() . '.' . $entity->id()); + $config->delete(); + } + + $this->postDelete($entities); + foreach ($entities as $id => $entity) { + $this->invokeHook('delete', $entity); + } + } + + /** + * Implements Drupal\entity\EntityStorageControllerInterface::save(). + */ + public function save(EntityInterface $entity) { + // Load the stored entity, if any. + if (!$entity->isNew() && !isset($entity->original) && $entity->getOriginalID()) { + $entity->original = entity_load_unchanged($this->entityType, $entity->getOriginalID()); + } + + $this->preSave($entity); + $this->invokeHook('presave', $entity); + + $config = config($entity::getConfigPrefix() . '.' . $entity->id()); + + // Configuration objects do not have a schema. Extract all key names from + // class properties. + $class_info = new \ReflectionClass($entity); + foreach ($class_info->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + $name = $property->getName(); + $config->set($name, $entity->$name); + } + + if (!$config->isNew()) { + $return = $config->save(); + $this->postSave($entity, TRUE); + $this->invokeHook('update', $entity); + } + else { + $return = $config->save(); + $entity->enforceIsNew(FALSE); + $this->postSave($entity, FALSE); + $this->invokeHook('insert', $entity); + } + + unset($entity->original); + + return $return; + } + + /** + * Acts on an entity before the presave hook is invoked. + * + * Used before the entity is saved and before invoking the presave hook. + */ + protected function preSave(EntityInterface $entity) { + } + + /** + * Acts on a saved entity before the insert or update hook is invoked. + * + * Used after the entity is saved, but before invoking the insert or update + * hook. + * + * @param $update + * (bool) TRUE if the entity has been updated, or FALSE if it has been + * inserted. + */ + protected function postSave(EntityInterface $entity, $update) { + // Delete the original configurable entity, in case the entity ID was + // renamed. + if ($update && !empty($entity->original) && $entity->{$this->idKey} !== $entity->original->{$this->idKey}) { + $entity->original->delete(); + } + } + + /** + * Acts on entities before they are deleted. + * + * Used before the entities are deleted and before invoking the delete hook. + */ + protected function preDelete($entities) { + } + + /** + * Acts on deleted entities before the delete hook is invoked. + * + * Used after the entities are deleted but before invoking the delete hook. + */ + protected function postDelete($entities) { + } + + /** + * Invokes a hook on behalf of the entity. + * + * @param $hook + * One of 'presave', 'insert', 'update', 'predelete', or 'delete'. + * @param $entity + * The entity object. + */ + protected function invokeHook($hook, EntityInterface $entity) { + // Invoke the hook. + module_invoke_all($this->entityType . '_' . $hook, $entity); + // Invoke the respective entity-level hook. + module_invoke_all('entity_' . $hook, $entity, $this->entityType); + } +} diff --git a/core/modules/config/lib/Drupal/config/ConfigurableBase.php b/core/modules/config/lib/Drupal/config/ConfigurableBase.php new file mode 100644 index 0000000..9ab007c --- /dev/null +++ b/core/modules/config/lib/Drupal/config/ConfigurableBase.php @@ -0,0 +1,90 @@ +id()) { + $this->originalID = $original_id; + } + } + + /** + * Implements ConfigurableInterface::getOriginalID(). + */ + public function getOriginalID() { + return $this->originalID; + } + + /** + * Overrides Entity::isNew(). + * + * EntityInterface::enforceIsNew() is not supported by configurable entities, + * since each Configurable is unique. + */ + final public function isNew() { + return !$this->id(); + } + + /** + * Overrides Entity::bundle(). + * + * EntityInterface::bundle() is not supported by configurable entities, since + * a Configurable is a bundle. + */ + final public function bundle() { + return $this->entityType; + } + + /** + * Overrides Entity::get(). + * + * EntityInterface::get() implements support for fieldable entities, but + * configurable entities are not fieldable. + */ + public function get($property_name, $langcode = NULL) { + // @todo: Add support for translatable properties being not fields. + return isset($this->{$property_name}) ? $this->{$property_name} : NULL; + } + + /** + * Overrides Entity::set(). + * + * EntityInterface::set() implements support for fieldable entities, but + * configurable entities are not fieldable. + */ + public function set($property_name, $value, $langcode = NULL) { + // @todo: Add support for translatable properties being not fields. + $this->{$property_name} = $value; + } + +} diff --git a/core/modules/config/lib/Drupal/config/ConfigurableInterface.php b/core/modules/config/lib/Drupal/config/ConfigurableInterface.php new file mode 100644 index 0000000..7d6075b --- /dev/null +++ b/core/modules/config/lib/Drupal/config/ConfigurableInterface.php @@ -0,0 +1,32 @@ + 'Configurable entities', + 'description' => 'Tests configurable entities.', + 'group' => 'Configuration', + ); + } + + /** + * Tests basic CRUD operations through the UI. + */ + function testCRUD() { + // Create a configurable entity. + $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 entity. + $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 entity. + $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 entity. + $edit = array( + 'id' => $id, + 'label' => 'Thingie', + ); + $this->drupalPost('admin/structure/config_test/add', $edit, 'Save'); + $this->assertResponse(200); + $this->assertText('Thingie'); + + // Rename the configurable entity'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 index ec92fdf..7265abaa 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php @@ -31,6 +31,33 @@ public static function getInfo() { ); } + function setUp() { + parent::setUp(); + + // Clear out any possibly existing hook invocation records. + unset($GLOBALS['hook_config_test']); + } + + /** + * 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'])); + + // Export. + config_export(); + + // Verify that config_export() does not involve module APIs. + $this->assertFalse(isset($GLOBALS['hook_config_test'])); + } + /** * Tests deletion of configuration during import. */ @@ -64,6 +91,14 @@ function testDeleted() { $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']['load'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['presave'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['insert'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['update'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['predelete'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['delete'])); } /** @@ -100,6 +135,14 @@ function testNew() { $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->assertFalse(isset($GLOBALS['hook_config_test']['load'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['presave'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['insert'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['update'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); } /** @@ -138,5 +181,13 @@ function testUpdated() { $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']['load'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['presave'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['insert'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['update'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['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..d730554 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 @@ public static function getInfo() { */ 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 @@ function testModuleInstallation() { // 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 entity. $this->assertTrue($GLOBALS['hook_config_import']); + + // Verify that config_test API hooks were invoked for the dynamic default + // configurable entity. + $this->assertFalse(isset($GLOBALS['hook_config_test']['load'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['presave'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['insert'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['update'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete'])); + $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); } } diff --git a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php index 2dbc627..e1d77be 100644 --- a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php +++ b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php @@ -32,6 +32,9 @@ function testCRUD() { $name = 'config_test.storage'; + // Checking whether a non-existing name exists returns FALSE. + $this->assertIdentical($this->storage->exists($name), FALSE); + // Reading a non-existing name returns FALSE. $data = $this->storage->read($name); $this->assertIdentical($data, FALSE); @@ -51,9 +54,13 @@ function testCRUD() { $data = array('foo' => 'bar'); $result = $this->storage->write($name, $data); $this->assertIdentical($result, TRUE); + $raw_data = $this->read($name); $this->assertIdentical($raw_data, $data); + // Checking whether an existing name exists returns TRUE. + $this->assertIdentical($this->storage->exists($name), TRUE); + // Writing the identical data again still returns TRUE. $result = $this->storage->write($name, $data); $this->assertIdentical($result, TRUE); 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..80f3381 --- /dev/null +++ b/core/modules/config/tests/config_test/config_test.hooks.inc @@ -0,0 +1,52 @@ +save(); + $config_test = entity_create('config_test', $new_config->get()); + $config_test->save(); return TRUE; } @@ -20,15 +23,26 @@ 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 - // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; } // Set a global value we can check in test code. $GLOBALS['hook_config_import'] = __FUNCTION__; - $new_config->save(); + // @todo Make this less ugly. + $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1); + $config_test = entity_load('config_test', $id); + + $config_test->original = clone $config_test; + foreach ($old_config->get() as $property => $value) { + $config_test->original->$property = $value; + } + + foreach ($new_config->get() as $property => $value) { + $config_test->$property = $value; + } + + $config_test->save(); return TRUE; } @@ -36,15 +50,239 @@ 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 - // settings can be synchronized directly. if (strpos($name, 'config_test.dynamic.') !== 0) { return FALSE; } // Set a global value we can check in test code. $GLOBALS['hook_config_import'] = __FUNCTION__; - $old_config->delete(); + $id = substr($name, strlen(ConfigTest::getConfigPrefix()) + 1); + config_test_delete($id); return TRUE; } +/** + * Implements hook_entity_info(). + */ +function config_test_entity_info() { + $types['config_test'] = array( + 'label' => 'Test configuration', + 'controller class' => 'Drupal\config\ConfigStorageController', + 'entity class' => 'Drupal\config_test\ConfigTest', + 'uri callback' => 'config_test_uri', + 'entity keys' => array( + 'id' => 'id', + 'label' => 'label', + 'uuid' => 'uuid', + ), + ); + return $types; +} + +/** + * Entity uri callback. + * + * @param Drupal\config_test\ConfigTest $config_test + * A ConfigTest entity. + */ +function config_test_uri(ConfigTest $config_test) { + return array( + 'path' => 'admin/structure/config_test/manage/' . $config_test->id(), + ); +} + +/** + * 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) { + return entity_load('config_test', $id); +} + +/** + * 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) { + entity_delete_multiple('config_test', array($id)); +} + +/** + * Page callback; Lists available ConfigTest objects. + */ +function config_test_list_page() { + $rows = array(); + foreach (entity_load_multiple('config_test', FALSE) as $config_test) { + $uri = $config_test->uri(); + $row = array(); + $row['name']['data'] = array( + '#type' => 'link', + '#title' => $config_test->label(), + '#href' => $uri['path'], + '#options' => $uri['options'], + ); + $row['delete']['data'] = array( + '#type' => 'link', + '#title' => t('Delete'), + '#href' => $uri['path'] . '/delete', + '#options' => $uri['options'], + ); + $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 = entity_create('config_test', array()); + } + $form_state['config_test'] = $config_test; + + $form['label'] = array( + '#type' => 'textfield', + '#title' => 'Label', + '#default_value' => $config_test->label(), + '#required' => TRUE, + ); + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $config_test->id(), + '#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->label()))); + } + else { + drupal_set_message(format_string('%label configuration has been created.', array('%label' => $config_test->label()))); + } + + $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->id()); + return confirm_form($form, + format_string('Are you sure you want to delete %label', array('%label' => $config_test->label())), + '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..104c11d --- /dev/null +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php @@ -0,0 +1,34 @@ +