Commit 42d5f005 authored by webchick's avatar webchick

Issue #1668820 by sun, xjm, tim.plunkett, fgm: Concept, base class, and...

Issue #1668820 by sun, xjm, tim.plunkett, fgm: Concept, base class, and interface for configurable objects.
parent 6d6e73a4
......@@ -3542,6 +3542,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'),
......
......@@ -354,6 +354,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;
......
......@@ -8,7 +8,7 @@
namespace Drupal\Core\File;
use Drupal\entity\DatabaseStorageController;
use Drupal\entity\EntityInterface;
use Drupal\entity\StorableInterface;
/**
* File storage controller for files.
......@@ -34,7 +34,7 @@ public function create(array $values) {
/**
* Overrides Drupal\entity\DatabaseStorageController::presave().
*/
protected function preSave(EntityInterface $entity) {
protected function preSave(StorableInterface $entity) {
$entity->timestamp = REQUEST_TIME;
$entity->filesize = filesize($entity->uri);
if (!isset($entity->langcode)) {
......
......@@ -7,7 +7,7 @@
namespace Drupal\comment;
use Drupal\entity\EntityInterface;
use Drupal\entity\StorableInterface;
use Drupal\entity\DatabaseStorageController;
use LogicException;
......@@ -58,7 +58,7 @@ protected function attachLoad(&$comments, $load_revision = FALSE) {
* @see comment_int_to_alphadecimal()
* @see comment_alphadecimal_to_int()
*/
protected function preSave(EntityInterface $comment) {
protected function preSave(StorableInterface $comment) {
global $user;
if (!isset($comment->status)) {
......@@ -151,7 +151,7 @@ protected function preSave(EntityInterface $comment) {
/**
* Overrides Drupal\entity\DatabaseStorageController::postSave().
*/
protected function postSave(EntityInterface $comment, $update) {
protected function postSave(StorableInterface $comment, $update) {
$this->releaseThreadLock();
// Update the {node_comment_statistics} table prior to executing the hook.
$this->updateNodeStatistics($comment->nid);
......
......@@ -30,13 +30,13 @@
* @param Drupal\Core\Config\Config $old_config
* 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
function hook_config_import_create($name, $new_config, $old_config) {
// 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;
}
......@@ -59,14 +59,28 @@ function MODULE_config_import_create($name, $new_config, $old_config) {
* @param Drupal\Core\Config\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
function hook_config_import_change($name, $new_config, $old_config) {
// 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.
list($entity_type) = explode('.', $name);
$entity_info = entity_get_info($entity_type);
$id = substr($name, strlen($entity_info['config prefix']) + 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;
}
......@@ -89,8 +103,8 @@ function MODULE_config_import_change($name, $new_config, $old_config) {
* @param Drupal\Core\Config\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
function hook_config_import_delete($name, $new_config, $old_config) {
// 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 +114,10 @@ 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();
list($entity_type) = explode('.', $name);
$entity_info = entity_get_info($entity_type);
$id = substr($name, strlen($entity_info['config prefix']) + 1);
config_test_delete($id);
return TRUE;
}
<?php
/**
* @file
* Definition of Drupal\config\ConfigurableBase.
*/
namespace Drupal\config;
use Drupal\entity\StorableBase;
/**
* Defines a base configurable entity class.
*/
abstract class ConfigurableBase extends StorableBase implements ConfigurableInterface {
/**
* The original ID of the configurable entity.
*
* The ID of a configurable entity is a unique string (machine name). When a
* configurable entity is updated and its machine name is renamed, the
* original ID needs to be known.
*
* @var string
*/
protected $originalID;
/**
* Overrides Entity::__construct().
*/
public function __construct(array $values = array(), $entity_type) {
parent::__construct($values, $entity_type);
// Backup the original ID, if any.
if ($original_id = $this->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;
}
/**
* Helper callback for uasort() to sort Configurable entities by weight and label.
*/
public static function sort($a, $b) {
$a_weight = isset($a->weight) ? $a->weight : 0;
$b_weight = isset($b->weight) ? $b->weight : 0;
if ($a_weight == $b_weight) {
$a_label = $a->label();
$b_label = $b->label();
return strnatcasecmp($a_label, $b_label);
}
return ($a_weight < $b_weight) ? -1 : 1;
}
}
<?php
/**
* @file
* Definition of Drupal\config\ConfigurableInterface.
*/
namespace Drupal\config;
use Drupal\entity\StorableInterface;
/**
* Defines the interface common for all configurable entities.
*/
interface ConfigurableInterface extends StorableInterface {
/**
* Returns the original ID.
*
* @return string|null
* The original ID, if any.
*/
public function getOriginalID();
}
<?php
/**
* @file
* Definition of Drupal\config\Tests\ConfigConfigurableTest.
*/
namespace Drupal\config\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests configurable entities.
*/
class ConfigConfigurableTest extends WebTestBase {
public static $modules = array('config_test');
public static function getInfo() {
return array(
'name' => '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');
}
}
......@@ -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,14 @@ 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']));
}
}
......@@ -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']));
}
}
<?php
/**
* @file
* Fake third-party hook implementations for ConfigTest entities.
*
* Testing the module/hook system is not the purpose of this test helper module.
* Therefore, this file implements hooks on behalf of config_test module for
* config_test entity hooks themselves.
*/
/**
* Implements hook_config_test_load().
*/
function config_test_config_test_load() {
$GLOBALS['hook_config_test']['load'] = __FUNCTION__;
}
/**
* Implements hook_config_test_presave().
*/
function config_test_config_test_presave() {
$GLOBALS['hook_config_test']['presave'] = __FUNCTION__;
}
/**
* Implements hook_config_test_insert().
*/
function config_test_config_test_insert() {
$GLOBALS['hook_config_test']['insert'] = __FUNCTION__;
}
/**
* Implements hook_config_test_update().
*/
function config_test_config_test_update() {
$GLOBALS['hook_config_test']['update'] = __FUNCTION__;
}
/**
* Implements hook_config_test_predelete().
*/
function config_test_config_test_predelete() {
$GLOBALS['hook_config_test']['predelete'] = __FUNCTION__;
}
/**
* Implements hook_config_test_delete().
*/
function config_test_config_test_delete() {
$GLOBALS['hook_config_test']['delete'] = __FUNCTION__;
}
<?php
use Drupal\config_test\ConfigTest;
require_once dirname(__FILE__) . '/config_test.hooks.inc';
/**
* Implements MODULE_config_import_create().
* Implements hook_config_import_create().
*/
function config_test_config_import_create($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();
$config_test = entity_create('config_test', $new_config->get());
$config_test->save();
return TRUE;
}
/**
* Implements MODULE_config_import_change().
* Implements hook_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.
list($entity_type) = explode('.', $name);
$entity_info = entity_get_info($entity_type);
$id = substr($name, strlen($entity_info['config prefix']) + 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;
}
/**
* Implements MODULE_config_import_delete().
* Implements hook_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();
// @todo Make this less ugly.
list($entity_type) = explode('.', $name);
$entity_info = entity_get_info($entity_type);
$id = substr($name, strlen($entity_info['config prefix']) + 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',
'config prefix' => 'config_test.dynamic',
'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() {
$entities = entity_load_multiple('config_test');
uasort($entities, 'Drupal\config\ConfigurableBase::sort');
$rows = array();
foreach ($entities as $config_test) {