diff --git a/lib/Drupal/ctools/ConfigExportableController.php b/lib/Drupal/ctools/ConfigExportableController.php index 29b0daf..5164dae 100644 --- a/lib/Drupal/ctools/ConfigExportableController.php +++ b/lib/Drupal/ctools/ConfigExportableController.php @@ -8,8 +8,346 @@ namespace Drupal\ctools; /** - * @todo. + * Controls exportable entities storing data in the Drupal Config system. */ class ConfigExportableController extends ExportableControllerBase { + protected $cache = array(); + protected $cacheAll = FALSE; + + /** + * The prefix for the config system, where this exportable is stored. + */ + protected $prefix; + + /** + * Implements Drupal\ctools\ExportableControllerInterface::__construct(). + */ + public function __construct($type, array $info) { + parent::__construct($type, $info); + + $this->prefix = $this->info['config prefix'] . '.'; + + // @todo Config has no schema. + $this->schema = drupal_get_schema($this->info['schema']); + } + + /** + * Load some number of exportable objects. + * + * This function will cache the objects, load subsidiary objects if necessary, + * check default objects in code and properly set them up. It will cache + * the results so that multiple calls to load the same objects + * will not cause problems. + * + * It attempts to reduce, as much as possible, the number of queries + * involved. + * + * @param $type + * A string to notify the loader what the argument is + * - all: load all items. This is the default. $args is unused. + * - keys: $args will be an array of specific named objects to load. + * - conditions: $args will be a keyed array of conditions. The conditions + * must be in the schema for this table or errors will result. + * @param $args + * An array of arguments whose actual use is defined by the $type argument. + */ + public function loadExportables($type = 'all', array $args = array()) { + // If fetching all and cached all, we've done so and we are finished. + if ($type == 'all' && !empty($this->cacheAll)) { + return $this->cache; + } + + $return = array(); + + // Don't load anything we've already cached. + if ($type == 'keys' && !empty($args)) { + foreach ($args as $index => $id) { + if (isset($this->cache[$id])) { + $return[$id] = $this->cache[$id]; + unset($args[$index]); + } + } + + // If nothing left to load, return the result. + if (empty($args)) { + return $return; + } + } + + $keys = array(); + if ($type == 'keys') { + foreach ($args as $id) { + $keys[$id] = $this->prefix . $id; + } + } + // For type 'all' and 'conditions', we need to load all config objects. + else { + $prefix_length = strlen($this->prefix); + foreach (config_get_storage_names_with_prefix($this->prefix) as $config_name) { + $id = substr($config_name, $prefix_length); + $keys[$id] = $config_name; + } + } + + foreach ($keys as $id => $config_name) { + $config = config($config_name); + if ($config->isNew()) { + continue; + } + $object = new $this->info['exportable class']($config, $this->type); + + $return[$id] = $this->cache[$id] = $object; + + // @todo Config has no way to distinguish the source of the data. + // We need a solution for this. + // @todo It's not clear at all what this comment ^^ means. + } + + // @todo -- if we do it this way we really shouldn't put them in + // $this->cache until after they've been altered. + if (method_exists($this, 'loadAlter')) { + $this->loadAlter($this->cache); + } + + // @todo Figure out what defaults are in this concept. + $defaults = $this->getDefaultExportables($args); + + if ($defaults) { + foreach ($defaults as $object) { + if ($type == 'keys') { + if (!in_array($object->id(), $args)) { + continue; + } + } + + // If we found a default but it's in the dtabase, mark it so. + if (!empty($this->cache[$object->id()])) { + $this->cache[$object->id()]->setIsInCode(TRUE); + $this->cache[$object->id()]->setExportModule($object->getExportModule()); + if ($type == 'conditions') { + $return[$object->id()] = $this->cache[$object->id()]; + } + } + else { + $object->setIsInCode(TRUE); + + $this->cache[$object->id()] = $object; + if ($type == 'conditions') { + $return[$object->id()] = $object; + } + } + } + } + + switch ($type) { + // If fetching all, we've done so and we are finished. + case 'all': + $this->cacheAll = TRUE; + return $this->cache; + + case 'keys': + foreach ($keys as $id => $config_name) { + if (isset($this->cache[$id])) { + $return[$id] = $this->cache[$id]; + } + } + break; + + case 'conditions': + foreach ($this->cache as $id => $exportable) { + $config = $exportable->getData(); + // If this does not match all of our conditions, skip it. + // @todo Smarter conditions would be a lot nicer here. + foreach ($args as $condition => $value) { + if ($config->get($condition) != $value) { + continue 2; + } + } + $return[$id] = $exportable; + } + break; + } + + // For conditions and keys. + return $return; + } + + public function getDefaultExportables(array $args = NULL) { + if (isset($this->cachedDefaults)) { + return $this->cachedDefaults; + } + + if ($this->info['default hook']) { + if (!empty($this->info['api'])) { + ctools_include('plugins'); + $info = ctools_plugin_api_include($this->info['api']['owner'], $this->info['api']['api'], + $this->info['api']['minimum_version'], $this->info['api']['current_version']); + $modules = array_keys($info); + } + else { + $modules = module_implements($this->info['default hook']); + } + + foreach ($modules as $module) { + $function = $module . '_' . $this->info['default hook']; + if (function_exists($function)) { + foreach ((array) $function($this->info) as $name => $data) { + $object = new $this->info['exportable class']($data, $this->type); + + // Record the module that provides this exportable. + $object->setExportModule($module); + + if (empty($this->info['api'])) { + $this->cachedDefaults[$name] = $object; + } + else { + // If version checking is enabled, ensure that the object can be used. + if (isset($object->api_version) && + version_compare($object->api_version, $this->info['api']['minimum_version']) >= 0 && + version_compare($object->api_version, $this->info['api']['current_version']) <= 0) { + $this->cachedDefaults[$name] = $object; + } + } + } + } + } + + drupal_alter($this->info['default hook'], $this->cachedDefaults); + } + return $this->cachedDefaults; + } + + /** + * Implements Drupal\ctools\ExportableControllerInterface::create(). + */ + public function create(array $data = array(), $set_defaults = TRUE) { + $config = config(NULL); + // Populate default values. + foreach ($this->schema['fields'] as $field => $info) { + // Get a default if nothing exists. + if (!isset($data[$field])) { + $data[$field] = ($set_defaults && !empty($info['default'])) ? $info['default'] : NULL; + } + else { + $config->set($field, $data[$field]); + } + } + + return new $this->info['exportable class']($config, $this->type); + } + + /** + * Implements Drupal\ctools\ExportableControllerInterface::save(). + */ + public function save(ExportableInterface $exportable) { + $config = $exportable->getData(); + $config->setName($this->prefix . $exportable->id())->save(); + } + + /** + * Implements Drupal\ctools\ExportableControllerInterface::delete(). + */ + public function delete(array $keys) { + $exportables = loadMultiple($keys); + + foreach ($exportables as $exportable) { + $exportable->getData()->delete(); + } + } + + /** + * Implements Drupal\ctools\ExportableControllerInterface::setStatus(). + */ + public function setStatus(ExportableInterface $exportable, $new_status) { + $exportable->disabled = $new_status; + $exportable->getData()->set('disabled', $new_status); + } + + /** + * Implements Drupal\ctools\ExportableControllerInterface::unpack(). + */ + public function unpack(ExportableInterface $exportable, $config) { + // @todo As long as DatabaseExportableController exists, + // Exportable::$data has to be an array. The current code looks like there + // is an indecision on whether DatabaseExportableController or alternative + // implementations are still going to be needed in the future. In any case, + // as long as that concept exists, the Exportable class cannot decide on + // the data format. + // If that remains to be the case, then all invocations of config() should + // be removed from unpack() and pack(), $config changed back to $data, and + // config() and Drupal\Core\Config\Config should only be involved in + // load(), save(), and delete(). + // In the opposite case, DatabaseExportableController should be deleted. + + foreach ($config as $field => $value) { + // Handle reserved keys first (properties on the exportable object). + if (in_array($field, $this->reserved_keys)) { + $exportable->{$field} = $value; + } + elseif (isset($this->schema['fields'][$field])) { + // We need to make sure if a field is unserialized, it is not an empty string. + if (!empty($this->schema['fields'][$field]['serialize']) && is_string($value)) { + $exportable->set($field, !empty($value) ? unserialize($value) : $value); + } + else { + $exportable->set($field, $value); + } + } + else { + $exportable->set($field, $value); + } + } + return; + + if (is_array($config)) { + $name = (isset($config[$this->info['key']]) ? $this->prefix . $config[$this->info['key']] : NULL); + $config = config($name); + } + + foreach ($this->reserved_keys as $property) { + $exportable->{$property} = $config->get($property); + } + + $exportable->setData($config->get()); + + // @todo -- do we want to allow nesting in schema? i.e, allow something + // like $schema['fields']['foo.bar.baz']? +// foreach (array_keys($this->schema['fields']) as $field) { +// $exportable->set($field, $config->get($field)); +// } + } + + /** + * Implements Drupal\ctools\ExportableControllerInterface::pack(). + */ + public function pack(ExportableInterface $exportable) { + $data = array(); +// $config = $exportable->getData(); + + $data['disabled'] = !$exportable->isEnabled(); + + foreach ($this->reserved_keys as $property) { + if (isset($exportable->{$property})) { + $data[$property] = $exportable->{$property}; +// $config->set($property, $exportable->{$property}); + } + } + + foreach ($this->schema['fields'] as $field => $info) { + $value = $exportable->get($field); + if (isset($value)) { + $data[$field] = $value; + } + else { + $data[$field] = isset($info['default']) ? $info['default'] : NULL; + // @todo not sure we bother setting NULL here. Will the key be there anyway? + // Otherwise, I think we need to set this regardless. +// $config->set($field, !empty($info['default']) ? $info['default'] : NULL); + } + } + + return $data; +// return $config; + } } diff --git a/lib/Drupal/ctools/DatabaseExportableController.php b/lib/Drupal/ctools/DatabaseExportableController.php index 2922ce9..2a28f60 100644 --- a/lib/Drupal/ctools/DatabaseExportableController.php +++ b/lib/Drupal/ctools/DatabaseExportableController.php @@ -8,7 +8,7 @@ namespace Drupal\ctools; /** - * @todo. + * Controls exportables that store their data in the MySQL database. */ class DatabaseExportableController extends ExportableControllerBase { protected $cache = array(); @@ -19,7 +19,6 @@ class DatabaseExportableController extends ExportableControllerBase { */ public function __construct($type, array $info) { parent::__construct($type, $info); - $this->type = $type; // @todo CTools had some code to work around schema caching issues // that we may need to replicate. These were particularly difficult @@ -213,7 +212,7 @@ class DatabaseExportableController extends ExportableControllerBase { return $return; } - function getDefaultExportables(array $args = NULL) { + public function getDefaultExportables(array $args = NULL) { if (isset($this->cachedDefaults)) { return $this->cachedDefaults; } @@ -243,7 +242,6 @@ class DatabaseExportableController extends ExportableControllerBase { } else { // If version checking is enabled, ensure that the object can be used. - print($this->info['api']); if (isset($object->api_version) && version_compare($object->api_version, $this->info['api']['minimum_version']) >= 0 && version_compare($object->api_version, $this->info['api']['current_version']) <= 0) { @@ -293,7 +291,8 @@ class DatabaseExportableController extends ExportableControllerBase { $exportable->setIsInDatabase(TRUE); } - return drupal_write_record($this->info['schema'], $exportable, $update); + $data = $exportable->getData(); + return drupal_write_record($this->info['schema'], $data, $update); } /** @@ -325,20 +324,24 @@ class DatabaseExportableController extends ExportableControllerBase { /** * Implements Drupal\ctools\ExportableControllerInterface::unpack(). */ - public function unpack(ExportableInterface $exportable, array $data) { + public function unpack(ExportableInterface $exportable, $data) { // Go through our schema and build correlations. foreach ($data as $field => $value) { - if (isset($this->schema['fields'][$field])) { + // Handle reserved keys first (properties on the exportable object). + if (in_array($field, $this->reserved_keys)) { + $exportable->{$field} = $value; + } + elseif (isset($this->schema['fields'][$field])) { // We need to make sure if a field is unserialized, it is not an empty string. if (!empty($this->schema['fields'][$field]['serialize']) && is_string($value)) { - $exportable->$field = !empty($value) ? unserialize($value) : $value; + $exportable->set($field, !empty($value) ? unserialize($value) : $value); } else { - $exportable->$field = $value; + $exportable->set($field, $value); } } else { - $exportable->$field = $value; + $exportable->set($field, $value); } } @@ -348,7 +351,7 @@ class DatabaseExportableController extends ExportableControllerBase { // Might just want drupal_get_schema here later on? $join_schema = ctools_export_get_schema($join['table']); foreach ($join['load'] as $field) { - $exportable->$field = !empty($join_schema['fields'][$field]['serialize']) ? unserialize($data[$field]) : $data[$field]; + $exportable->set($field, !empty($join_schema['fields'][$field]['serialize']) ? unserialize($data[$field]) : $data[$field]); } } } @@ -370,11 +373,12 @@ class DatabaseExportableController extends ExportableControllerBase { } foreach ($this->schema['fields'] as $field => $info) { - if (isset($exportable->{$field})) { - $data[$field] = $exportable->{$field}; + $value = $exportable->get($field); + if (isset($value)) { + $data[$field] = $value; } else { - $data[$field] = !empty($info['default']) ? $info['default'] : NULL; + $data[$field] = isset($info['default']) ? $info['default'] : NULL; } } diff --git a/lib/Drupal/ctools/Exportable.php b/lib/Drupal/ctools/Exportable.php index 2519249..faa4382 100644 --- a/lib/Drupal/ctools/Exportable.php +++ b/lib/Drupal/ctools/Exportable.php @@ -41,11 +41,23 @@ class Exportable implements ExportableInterface { protected $exportableModule; /** + * Stores the API version of the exportable. + * + * @var string + */ + public $api_version; + + /** * Stores the disabled state of the exportable. * * @var bool */ - protected $disabled = FALSE; + public $disabled = FALSE; + + /** + * The original data used to create this exportable. + */ + protected $data; /** * @todo. @@ -55,18 +67,30 @@ class Exportable implements ExportableInterface { $this->exportableType = $exportableType; } - $disabled = FALSE; + $this->unpack($data); + } - if (isset($data['disabled'])) { - $disabled = $data['disabled']; - unset($data['disabled']); - } + /** + * @todo + */ + // @todo Duplicates ::pack(). + public function getData() { + return $this->data; + } - $this->unpack($data); + // @todo Duplicates ::unpack(). + public function setData($data) { + $this->data = $data; + return $this; + } - $info = ctools_exportable_get_controller($this->exportableType)->getInfo(); - $status = variable_get($info['status'], array()); - $this->disabled = isset($status[$this->id()]) ? $status[$this->id()] : $disabled; + public function get($field) { + return isset($this->data[$field]) ? $this->data[$field] : NULL; + } + + public function set($field, $value) { + $this->data[$field] = $value; + return $this; } /** @@ -195,7 +219,7 @@ class Exportable implements ExportableInterface { */ public function id() { $info = ctools_exportable_get_controller($this->exportableType)->getInfo(); - return $this->{$info['key']}; + return $this->get($info['key']); } /** @@ -203,7 +227,7 @@ class Exportable implements ExportableInterface { */ public function title() { $info = ctools_exportable_get_controller($this->exportableType)->getInfo(); - return $this->{$info['title key']}; + return $this->get($info['title key']); } } diff --git a/lib/Drupal/ctools/ExportableControllerInterface.php b/lib/Drupal/ctools/ExportableControllerInterface.php index 8d2779e..1adf39d 100644 --- a/lib/Drupal/ctools/ExportableControllerInterface.php +++ b/lib/Drupal/ctools/ExportableControllerInterface.php @@ -192,10 +192,10 @@ interface ExportableControllerInterface { * * @param \Drupal\ctools\ExportableInterface $exportable * The exportable to unpack the data into. - * @param array $data - * An array of data to unpack onto the exportable. + * @param mixed $data + * An array or config object of data to unpack onto the exportable. */ - public function unpack(ExportableInterface $exportable, array $data); + public function unpack(ExportableInterface $exportable, $data); /** * Extracts properties from an exportable. diff --git a/lib/Drupal/ctools/ExportableInterface.php b/lib/Drupal/ctools/ExportableInterface.php index 6edf72d..080c9ec 100644 --- a/lib/Drupal/ctools/ExportableInterface.php +++ b/lib/Drupal/ctools/ExportableInterface.php @@ -108,4 +108,15 @@ interface ExportableInterface { */ public function title(); + /** + * @todo. + */ + public function getData(); + + public function setData($data); + + public function get($field); + + public function set($field, $value); + } diff --git a/lib/Drupal/ctools/Tests/ExportableCrudTest.php b/lib/Drupal/ctools/Tests/ExportableCrudTest.php index ac3d68e..224957d 100644 --- a/lib/Drupal/ctools/Tests/ExportableCrudTest.php +++ b/lib/Drupal/ctools/Tests/ExportableCrudTest.php @@ -131,7 +131,7 @@ class ExportableCrudTest extends WebTestBase { $this->assertEqual($expected_export, $loaded_export, 'An exportable object has been loaded correctly from the database.'); - $this->assertTrue(is_array($loaded_export->data), 'Serialized data has been unserialized on the exportable.'); + $this->assertTrue(is_array($loaded_export->get('data')), 'Serialized data has been unserialized on the exportable.'); // Load an overridden exportable. $expected_export = new $info['exportable class'](array( diff --git a/tests/ctools_export_test/ctools_export_test.module b/tests/ctools_export_test/ctools_export_test.module index b6fc7dd..9663ccc 100644 --- a/tests/ctools_export_test/ctools_export_test.module +++ b/tests/ctools_export_test/ctools_export_test.module @@ -12,7 +12,13 @@ function ctools_export_test_ctools_plugin_api($module, $api) { function ctools_export_test_ctools_exportable_info() { return array( 'ctools_export_test' => array( - 'controller class' => 'Drupal\ctools\DatabaseExportableController', + // @todo Change ExportableCrudTest into a base test and extend it for the + // Database and Config exportable controllers, so the same test + // procedure is run for each controller. + // @see Drupal\config\Tests\Storage\ConfigStorageTestBase + //'controller class' => 'Drupal\ctools\DatabaseExportableController', + 'controller class' => 'Drupal\ctools\ConfigExportableController', + 'config prefix' => 'ctools_export_test.items', 'key' => 'machine', 'identifier' => 'ctools_export_test', 'default hook' => 'default_ctools_export_tests',