diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php index bdeeba1..b3b51c2 100644 --- a/core/lib/Drupal/Core/Config/Config.php +++ b/core/lib/Drupal/Core/Config/Config.php @@ -110,7 +110,7 @@ public function get($key = '') { * {@inheritdoc} */ public function setData(array $data) { - $this->data = $data; + parent::setData($data); $this->resetOverriddenData(); return $this; } diff --git a/core/lib/Drupal/Core/Config/ConfigBase.php b/core/lib/Drupal/Core/Config/ConfigBase.php index 3508611..37e0d80 100644 --- a/core/lib/Drupal/Core/Config/ConfigBase.php +++ b/core/lib/Drupal/Core/Config/ConfigBase.php @@ -160,13 +160,39 @@ public function get($key = '') { * * @return $this * The configuration object. + * + * @throws \Drupal\Core\Config\ConfigValueException + * If any key in $data in any depth contains a dot. */ public function setData(array $data) { + $this->validateKeys($data); $this->data = $data; return $this; } /** + * Validate all keys in a passed in config array structure. + * + * @param array $data + * Configuration array structure. + * + * @return null + * + * @throws \Drupal\Core\Config\ConfigValueException + * If any key in $data in any depth contains a dot. + */ + protected function validateKeys(array $data) { + foreach ($data as $key => $value) { + if (strpos($key, '.') !== FALSE) { + throw new ConfigValueException(String::format('@key key contains a dot which is not supported.', array('@key' => $key))); + } + if (is_array($value)) { + $this->validateKeys($value); + } + } + } + + /** * Sets a value in this configuration object. * * @param string $key @@ -176,10 +202,16 @@ public function setData(array $data) { * * @return $this * The configuration object. + * + * @throws \Drupal\Core\Config\ConfigValueException + * If $value is an array and any of its keys in any depth contains a dot. */ public function set($key, $value) { // The dot/period is a reserved character; it may appear between keys, but // not within keys. + if (is_array($value)) { + $this->validateKeys($value); + } $parts = explode('.', $key); if (count($parts) == 1) { $this->data[$key] = $value; diff --git a/core/lib/Drupal/Core/Config/ConfigValueException.php b/core/lib/Drupal/Core/Config/ConfigValueException.php new file mode 100644 index 0000000..9802228 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigValueException.php @@ -0,0 +1,13 @@ +configFactory->loadMultiple($names) as $config) { - $records[$config->get($this->idKey)] = $config->get(); + $records[$config->get($this->idKey)] = $this->postLoadData($config->get()); } return $this->mapFromStorageRecords($records); } @@ -237,7 +237,7 @@ protected function doSave($id, EntityInterface $entity) { } // Retrieve the desired properties and set them in config. - foreach ($entity->toArray() as $key => $value) { + foreach ($this->preSaveData($entity->toArray(), $entity) as $key => $value) { $config->set($key, $value); } $config->save(); @@ -246,6 +246,32 @@ protected function doSave($id, EntityInterface $entity) { } /** + * Alter data for the storage environment right before saving. + * + * @param array $data + * Raw configuration data being saved. + * + * @return array + * Data to save with any modifications necessary for storage performed. + */ + protected function preSaveData(array $data) { + return $data; + } + + /** + * Alter data from the storage environment right after loading. + * + * @param array $data + * Raw configuration data as loaded. + * + * @return array + * Transformed data with any modifications necessary for instantiation. + */ + protected function postLoadData(array $data) { + return $data; + } + + /** * {@inheritdoc} */ protected function has($id, EntityInterface $entity) { diff --git a/core/lib/Drupal/Core/Field/FieldItemBase.php b/core/lib/Drupal/Core/Field/FieldItemBase.php index 296c468..f168001 100644 --- a/core/lib/Drupal/Core/Field/FieldItemBase.php +++ b/core/lib/Drupal/Core/Field/FieldItemBase.php @@ -255,4 +255,24 @@ public function instanceSettingsForm(array $form, array &$form_state) { return array(); } + /** + * {@inheritdoc} + */ + public static function preSaveSettings(array &$settings) { } + + /** + * {@inheritdoc} + */ + public static function postLoadSettings(array &$settings) { } + + /** + * {@inheritdoc} + */ + public static function preSaveInstanceSettings(array &$settings) { } + + /** + * {@inheritdoc} + */ + public static function postLoadInstanceSettings(array &$settings) { } + } diff --git a/core/lib/Drupal/Core/Field/FieldItemInterface.php b/core/lib/Drupal/Core/Field/FieldItemInterface.php index ee27996..8b624c7 100644 --- a/core/lib/Drupal/Core/Field/FieldItemInterface.php +++ b/core/lib/Drupal/Core/Field/FieldItemInterface.php @@ -183,6 +183,48 @@ public function view($display_options = array()); public function preSave(); /** + * Defines behavior for transforming field settings before being saved. + * + * May be used to alter settings to transform them to a storage-friendly form. + * + * @param array $settings + * Field settings. + */ + public static function preSaveSettings(array &$settings); + + /** + * Defines custom behavior for transforming field settings when loading. + * + * May be used to alter settings to transform them from a storage-friendly + * form. + * + * @param array $settings + * Field settings. + */ + public static function postLoadSettings(array &$settings); + + /** + * Defines behavior for transforming instance settings before being saved. + * + * May be used to alter settings to transform them to a storage-friendly form. + * + * @param array $settings + * Field settings. + */ + public static function preSaveInstanceSettings(array &$settings); + + /** + * Defines custom behavior for transforming instance settings when loading. + * + * May be used to alter settings to transform them from a storage-friendly + * form. + * + * @param array $settings + * Field settings. + */ + public static function postLoadInstanceSettings(array &$settings); + + /** * Defines custom insert behavior for field values. * * This method is called during the process of inserting an entity, just diff --git a/core/modules/config/src/Tests/ConfigCRUDTest.php b/core/modules/config/src/Tests/ConfigCRUDTest.php index 122843e..c38d3f6 100644 --- a/core/modules/config/src/Tests/ConfigCRUDTest.php +++ b/core/modules/config/src/Tests/ConfigCRUDTest.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\String; use Drupal\Core\Config\ConfigNameException; +use Drupal\Core\Config\ConfigValueException; use Drupal\Core\Config\InstallStorage; use Drupal\simpletest\DrupalUnitTestBase; use Drupal\Core\Config\FileStorage; @@ -184,6 +185,31 @@ function testNameValidation() { } /** + * Tests the validation of configuration object values. + */ + function testValueValidation() { + // Verify that setData() will catch dotted keys. + $message = 'Expected ConfigValueException was thrown from setData() for value with dotted keys.'; + try { + \Drupal::config('namespace.object')->setData(array('key.value' => 12))->save(); + $this->fail($message); + } + catch (ConfigValueException $e) { + $this->pass($message); + } + + // Verify that set() will catch dotted keys. + $message = 'Expected ConfigValueException was thrown from set() for value with dotted keys.'; + try { + \Drupal::config('namespace.object')->set('foo', array('key.value' => 12))->save(); + $this->fail($message); + } + catch (ConfigValueException $e) { + $this->pass($message); + } + } + + /** * Tests data type handling. */ public function testDataTypes() { diff --git a/core/modules/field/src/Entity/FieldInstanceConfig.php b/core/modules/field/src/Entity/FieldInstanceConfig.php index 79830db..3a5314d 100644 --- a/core/modules/field/src/Entity/FieldInstanceConfig.php +++ b/core/modules/field/src/Entity/FieldInstanceConfig.php @@ -253,7 +253,7 @@ public function __construct(array $values, $entity_type = 'field_instance_config } // Discard the 'field_type' entry that is added in config records to ease - // schema generation. See self::toArray(). + // schema generation and data altering for storage. See self::toArray(). unset($values['field_type']); parent::__construct($values, $entity_type); @@ -287,7 +287,8 @@ public function getType() { public function toArray() { $properties = parent::toArray(); // Additionally, include the field type, that is needed to be able to - // generate the field-type-dependant parts of the config schema. + // generate the field-type-dependant parts of the config schema and to + // allow for altering data by field type. $properties['field_type'] = $this->getType(); return $properties; diff --git a/core/modules/field/src/FieldStorageConfigStorage.php b/core/modules/field/src/FieldStorageConfigStorage.php index 8419424..18216dc 100644 --- a/core/modules/field/src/FieldStorageConfigStorage.php +++ b/core/modules/field/src/FieldStorageConfigStorage.php @@ -10,6 +10,7 @@ use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Config\Entity\ConfigEntityStorage; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Language\LanguageManagerInterface; @@ -156,4 +157,27 @@ public function loadByProperties(array $conditions = array()) { return $matches; } + + /** + * {@inheritdoc} + */ + protected function postLoadData(array $data) { + // @todo Makes too much assumptions but it needs to. + $type_definition = \Drupal::typedDataManager() + ->getDefinition('field_item:' . $data['type']); + $type_definition['class']::postLoadSettings($data['settings']); + return $data; + } + + /** + * {@inheritdoc} + */ + protected function preSaveData(array $data) { + // @todo Makes too much assumptions but it needs to. + $type_definition = \Drupal::typedDataManager() + ->getDefinition('field_item:' . $data['type']); + $type_definition['class']::preSaveSettings($data['settings']); + return $data; + } + } diff --git a/core/modules/field/src/FieldInstanceConfigStorage.php b/core/modules/field/src/FieldInstanceConfigStorage.php index 20dc083..26dc9f4 100644 --- a/core/modules/field/src/FieldInstanceConfigStorage.php +++ b/core/modules/field/src/FieldInstanceConfigStorage.php @@ -177,4 +177,25 @@ public function loadByProperties(array $conditions = array()) { return $matching_instances; } + /** + * {@inheritdoc} + */ + protected function postLoadData(array $data) { + // @todo Makes too much assumptions but it needs to. + $type_definition = \Drupal::typedDataManager() + ->getDefinition('field_item:' . $data['field_type']); + $type_definition['class']::postLoadInstanceSettings($data['settings']); + return $data; + } + + /** + * {@inheritdoc} + */ + protected function preSaveData(array $data) { + // @todo Makes too much assumptions but it needs to. + $type_definition = \Drupal::typedDataManager() + ->getDefinition('field_item:' . $data['field_type']); + $type_definition['class']::preSaveInstanceSettings($data['settings']); + return $data; + } } diff --git a/core/modules/options/config/schema/options.schema.yml b/core/modules/options/config/schema/options.schema.yml index 39ed439..7a639cb 100644 --- a/core/modules/options/config/schema/options.schema.yml +++ b/core/modules/options/config/schema/options.schema.yml @@ -8,8 +8,15 @@ field.list_integer.settings: type: sequence label: 'Allowed values list' sequence: - - type: string - label: 'Value' + - type: mapping + label: 'Allowed value with label' + mapping: + value: + type: integer + label: 'Value' + label: + type: label + label: 'Label' allowed_values_function: type: string label: 'Allowed values function' @@ -35,8 +42,18 @@ field.list_float.settings: label: 'List (float) settings' mapping: allowed_values: - type: ignore + type: sequence label: 'Allowed values list' + sequence: + - type: mapping + label: 'Allowed value with label' + mapping: + value: + type: float + label: 'Value' + label: + type: label + label: 'Label' allowed_values_function: type: string label: 'Allowed values function' @@ -65,8 +82,15 @@ field.list_text.settings: type: sequence label: 'Allowed values list' sequence: - - type: string - label: 'Value' + - type: mapping + label: 'Allowed value with label' + mapping: + value: + type: string + label: 'Value' + label: + type: label + label: 'Label' allowed_values_function: type: string label: 'Allowed values function' diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php b/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php index 30cd382..1b2d323 100644 --- a/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php +++ b/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php @@ -90,4 +90,24 @@ protected static function validateAllowedValue($option) { } } + /** + * {@inheritdoc} + */ + public static function simplifyAllowedValues(array $structured_values) { + $values = array(); + foreach ($structured_values as $item) { + // Nested elements are embedded in the label. + // See ListItemBase::structureAllowedValues(). + if (is_array($item['label'])) { + $item['label'] = static::simplifyAllowedValues($item['label']); + } + // Cast the value to a float first so that .5 and 0.5 are the same value + // and then cast to a string so that values like 0.5 can be used as array + // keys. + // @see http://php.net/manual/en/language.types.array.php + $values[(string) (float) $item['value']] = $item['label']; + } + return $values; + } + } diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php index d61aef1..f63b17f 100644 --- a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php +++ b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php @@ -237,4 +237,71 @@ protected function allowedValuesString($values) { return implode("\n", $lines); } + /** + * Defined in \Drupal\Core\Field\FieldItemInterface. + */ + public static function preSaveSettings(array &$settings) { + if (isset($settings['allowed_values'])) { + $settings['allowed_values'] = static::structureAllowedValues($settings['allowed_values']); + } + } + + /** + * Defined in \Drupal\Core\Field\FieldItemInterface. + */ + public static function postLoadSettings(array &$settings) { + if (isset($settings['allowed_values'])) { + $settings['allowed_values'] = static::simplifyAllowedValues($settings['allowed_values']); + } + } + + /** + * Simplify allowed values to a key-value array from the structured array. + * + * @param array $structured_values + * Array of items with a 'value' and 'label' key each for the allowed + * values. + * + * @return array + * Allowed values were the array key is the 'value' value, the value is + * the 'label' value. + */ + protected static function simplifyAllowedValues(array $structured_values) { + $values = array(); + foreach ($structured_values as $item) { + if (is_array($item['label'])) { + // Nested elements are embedded in the label. + // See structureAllowedValues(). + $item['label'] = static::simplifyAllowedValues($item['label']); + } + $values[$item['value']] = $item['label']; + } + return $values; + } + + /** + * Creates a structured array of allowed values from a key-value array. + * + * @param array $values + * Allowed values were the array key is the 'value' value, the value is + * the 'label' value. + * + * @return array + * Array of items with a 'value' and 'label' key each for the allowed + * values. + */ + protected static function structureAllowedValues(array $values) { + $structured_values = array(); + foreach ($values as $value => $label) { + if (is_array($label)) { + $label = static::structureAllowedValues($label); + } + $structured_values[] = array( + 'value' => $value, + 'label' => $label, + ); + } + return $structured_values; + } + } diff --git a/core/modules/options/src/Tests/OptionsFieldUITest.php b/core/modules/options/src/Tests/OptionsFieldUITest.php index c69e797..0eac490 100644 --- a/core/modules/options/src/Tests/OptionsFieldUITest.php +++ b/core/modules/options/src/Tests/OptionsFieldUITest.php @@ -94,6 +94,11 @@ function testOptionsAllowedValuesInteger() { $string = "0|Zero"; $array = array('0' => 'Zero'); $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); + + // Check that the same key can only be used once. + $string = "0|Zero\n0|One"; + $array = array('0' => 'One'); + $this->assertAllowedValuesInput($string, $array, 'Same value cannot be used multiple times.'); } /** @@ -144,6 +149,16 @@ function testOptionsAllowedValuesFloat() { $string = "0|Zero"; $array = array('0' => 'Zero'); $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); + + // Check that the same key can only be used once. + $string = "0.5|Point five\n0.5|Half"; + $array = array('0.5' => 'Half'); + $this->assertAllowedValuesInput($string, $array, 'Same value cannot be used multiple times.'); + + // Check that different forms of the same float value cannot be used. + $string = "0|Zero\n.5|Point five\n0.5|Half"; + $array = array('0' => 'Zero', '0.5' => 'Half'); + $this->assertAllowedValuesInput($string, $array, 'Different forms of the same value cannot be used.'); } /** @@ -199,6 +214,16 @@ function testOptionsAllowedValuesText() { $string = "Zero"; $array = array('Zero' => 'Zero'); $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); + + // Check that string values with dots can be used. + $string = "Zero\nexample.com|Example"; + $array = array('Zero' => 'Zero', 'example.com' => 'Example'); + $this->assertAllowedValuesInput($string, $array, 'String value with dot is supported.'); + + // Check that the same key can only be used once. + $string = "zero|Zero\nzero|One"; + $array = array('zero' => 'One'); + $this->assertAllowedValuesInput($string, $array, 'Same value cannot be used multiple times.'); } /**