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 @@ +setDescription(t('The length of time between feed updates. Requires a correctly configured cron maintenance task.', array('@cron' => url('admin/reports/status')))) ->setSetting('unsigned', TRUE) ->setRequired(TRUE) - ->setSetting('allowed_values', $period) + ->setSetting('allowed_values', ListItemBase::structureAllowedValues($period)) ->setDisplayOptions('form', array( 'type' => 'options_select', 'weight' => -2, 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/Tests/FormTest.php b/core/modules/field/src/Tests/FormTest.php index 2830abe..1e7bc0b 100644 --- a/core/modules/field/src/Tests/FormTest.php +++ b/core/modules/field/src/Tests/FormTest.php @@ -352,7 +352,7 @@ function testFieldFormMultivalueWithRequiredRadio() { 'entity_type' => 'entity_test', 'type' => 'list_text', 'settings' => array( - 'allowed_values' => array('yes' => 'yes', 'no' => 'no'), + 'allowed_values' => array(array('value' => 'yes', 'label' => 'yes'), array('value' => 'no', 'label' => 'no')), ), ))->save(); $instance = array( 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/options.module b/core/modules/options/options.module index d9a1b24..d0b6c00 100644 --- a/core/modules/options/options.module +++ b/core/modules/options/options.module @@ -11,6 +11,7 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\field\FieldStorageConfigInterface; +use Drupal\options\Plugin\Field\FieldType\ListItemBase; /** * Implements hook_help(). @@ -60,8 +61,9 @@ function options_field_config_delete(FieldConfigInterface $field) { * The entity object. * * @return - * The array of allowed values. Keys of the array are the raw stored values - * (number or text), values of the array are the display labels. + * The array of allowed values. Each array element is an associative + * array with 'value' and 'label' keys. The 'value' key stores the raw stored + * value (number or text), the 'label' key stores the display labels. */ function options_allowed_values(FieldDefinitionInterface $field_definition, EntityInterface $entity) { $allowed_values = &drupal_static(__FUNCTION__, array()); @@ -99,7 +101,11 @@ function options_field_config_update_forbid(FieldStorageConfigInterface $field_s // Forbid any update that removes allowed values with actual data. $allowed_values = $field_storage->getSetting('allowed_values'); $prior_allowed_values = $prior_field_storage->getSetting('allowed_values'); - $lost_keys = array_diff(array_keys($prior_allowed_values), array_keys($allowed_values)); + $type_definition = \Drupal::typedDataManager()->getDefinition('field_item:' . $field_storage->getType()); + $lost_keys = array_diff( + array_keys($type_definition['class']::simplifyAllowedValues($prior_allowed_values)), + array_keys($type_definition['class']::simplifyAllowedValues($allowed_values)) + ); if (_options_values_in_use($field_storage->entity_type, $field_storage->getName(), $lost_keys)) { throw new FieldStorageDefinitionUpdateForbiddenException(t('A list field (@field_name) with existing data cannot have its keys changed.', array('@field_name' => $field_storage->getName()))); } diff --git a/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php b/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php index 5118e5f..82353ed 100644 --- a/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php +++ b/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php @@ -7,8 +7,12 @@ namespace Drupal\options\Plugin\Field\FieldFormatter; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FormatterBase; use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\TypedData\TypedDataManager; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Plugin implementation of the 'list_default' formatter. @@ -23,7 +27,48 @@ * } * ) */ -class OptionsDefaultFormatter extends FormatterBase { +class OptionsDefaultFormatter extends FormatterBase implements ContainerFactoryPluginInterface { + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['label'], + $configuration['view_mode'], + $configuration['third_party_settings'], + $container->get('typed_data_manager') + ); + } + + /** + * Constructs a new OptionsDefaultFormatter. + * + * @param string $plugin_id + * The plugin_id for the formatter. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The definition of the field to which the formatter is associated. + * @param array $settings + * The formatter settings. + * @param string $label + * The formatter label display setting. + * @param string $view_mode + * The view mode. + * @param array $third_party_settings + * Third party settings. + * @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager + * The typed data manager. + */ + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, TypedDataManager $typed_data_manager) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings); + $this->typedDataManager = $typed_data_manager; + } /** * {@inheritdoc} @@ -32,7 +77,8 @@ public function viewElements(FieldItemListInterface $items) { $elements = array(); $entity = $items->getEntity(); - $allowed_values = options_allowed_values($this->fieldDefinition, $entity); + $type_definition = $this->typedDataManager->getDefinition('field_item:' . $this->fieldDefinition->getType()); + $allowed_values = $type_definition['class']::simplifyAllowedValues(options_allowed_values($this->fieldDefinition, $entity)); foreach ($items as $delta => $item) { if (isset($allowed_values[$item->value])) { diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php b/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php index 30cd382..195e1c7 100644 --- a/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php +++ b/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php @@ -69,16 +69,13 @@ protected function allowedValuesDescription() { protected static function extractAllowedValues($string, $has_data) { $values = parent::extractAllowedValues($string, $has_data); if ($values) { - $keys = array_keys($values); - $labels = array_values($values); - $keys = array_map(function ($key) { + foreach ($values as $item) { // Float keys are represented as strings and need to be disambiguated // ('.5' is '0.5'). - return is_numeric($key) ? (string) (float) $key : $key; - }, $keys); - - return array_combine($keys, $labels); + $item['value'] = is_numeric($item['value']) ? (string) (float) $item['value'] : $item['value']; + } } + return $values; } /** @@ -90,4 +87,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..7905045 100644 --- a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php +++ b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php @@ -59,7 +59,7 @@ public function getSettableValues(AccountInterface $account = NULL) { */ public function getSettableOptions(AccountInterface $account = NULL) { $allowed_options = options_allowed_values($this->getFieldDefinition(), $this->getEntity()); - return $allowed_options; + return static::simplifyAllowedValues($allowed_options); } /** @@ -129,16 +129,25 @@ public static function validateAllowedValues($element, &$form_state) { } else { // Check that keys are valid for the field type. - foreach ($values as $key => $value) { - if ($error = static::validateAllowedValue($key)) { + $keys_in_use = array(); + foreach ($values as $item) { + if ($error = static::validateAllowedValue($item['value'])) { \Drupal::formBuilder()->setError($element, $form_state, $error); break; } + if (in_array($item['value'], $keys_in_use)) { + \Drupal::formBuilder()->setError($element, $form_state, t('One key can only be used once.')); + break; + } + $keys_in_use[] = $item['value']; } // Prevent removing values currently in use. if ($element['#field_has_data']) { - $lost_keys = array_diff(array_keys($element['#allowed_values']), array_keys($values)); + $lost_keys = array_diff( + array_keys(static::simplifyAllowedValues($element['#allowed_values'])), + array_keys(static::simplifyAllowedValues($values)) + ); if (_options_values_in_use($element['#entity_type'], $element['#field_name'], $lost_keys)) { \Drupal::formBuilder()->setError($element, $form_state, t('Allowed values list: some values are being removed while currently in use.')); } @@ -193,7 +202,7 @@ protected static function extractAllowedValues($string, $has_data) { return; } - $values[$key] = $value; + $values[] = array('value' => $key, 'label' => $value); } // We generate keys only if the list contains no explicit key at all. @@ -220,21 +229,70 @@ protected static function validateAllowedValue($option) { } * * This string format is suitable for edition in a textarea. * - * @param array $values - * An array of values, where array keys are values and array values are - * labels. + * @param array $structured_values + * An array of values, where elements are arrays with 'value' and 'label' + * keys. * * @return string * The string representation of the $values array: * - Values are separated by a carriage return. - * - Each value is in the format "value|label" or "value". + * - Each value is in the format "value|label". */ - protected function allowedValuesString($values) { + protected function allowedValuesString($structured_values) { $lines = array(); - foreach ($values as $key => $value) { - $lines[] = "$key|$value"; + foreach ($structured_values as $item) { + $lines[] = $item['value'] . '|' . $item['label']; } return implode("\n", $lines); } + /** + * 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. + */ + public 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. + */ + public 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/OptionsFieldTest.php b/core/modules/options/src/Tests/OptionsFieldTest.php index 925d5ce..e6c44a7 100644 --- a/core/modules/options/src/Tests/OptionsFieldTest.php +++ b/core/modules/options/src/Tests/OptionsFieldTest.php @@ -39,7 +39,7 @@ function testUpdateAllowedValues() { $entity = entity_create('entity_test'); $entity->{$this->fieldName}->value = 1; $entity->save(); - $this->fieldStorage->settings['allowed_values'] = array(2 => 'Two'); + $this->fieldStorage->settings['allowed_values'] = array(array('value' => 2, 'label' => 'Two')); try { $this->fieldStorage->save(); $this->fail(t('Cannot update a list field storage to not include keys with existing data.')); @@ -52,7 +52,7 @@ function testUpdateAllowedValues() { $entity->save(); // Removed options do not appear. - $this->fieldStorage->settings['allowed_values'] = array(2 => 'Two'); + $this->fieldStorage->settings['allowed_values'] = array(array('value' => 2, 'label' => 'Two')); $this->fieldStorage->save(); $entity = entity_create('entity_test'); $form = \Drupal::service('entity.form_builder')->getForm($entity); @@ -61,7 +61,7 @@ function testUpdateAllowedValues() { $this->assertTrue(empty($form[$this->fieldName]['widget'][3]), 'Option 3 does not exist'); // Completely new options appear. - $this->fieldStorage->settings['allowed_values'] = array(10 => 'Update', 20 => 'Twenty'); + $this->fieldStorage->settings['allowed_values'] = array(array('value' => 10, 'label' => 'Update'), array('value' => 20, 'label' => 'Twenty')); $this->fieldStorage->save(); // The entity holds an outdated field object with the old allowed values // setting, so we need to reintialize the entity object. diff --git a/core/modules/options/src/Tests/OptionsFieldUITest.php b/core/modules/options/src/Tests/OptionsFieldUITest.php index c69e797..1292dd2 100644 --- a/core/modules/options/src/Tests/OptionsFieldUITest.php +++ b/core/modules/options/src/Tests/OptionsFieldUITest.php @@ -54,15 +54,24 @@ function testOptionsAllowedValuesInteger() { // Flat list of textual values. $string = "Zero\nOne"; - $array = array('0' => 'Zero', '1' => 'One'); + $array = array( + array('value' => 0, 'label' => 'Zero'), + array('value' => 1, 'label' => 'One'), + ); $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.'); // Explicit integer keys. $string = "0|Zero\n2|Two"; - $array = array('0' => 'Zero', '2' => 'Two'); + $array = array( + array('value' => 0, 'label' => 'Zero'), + array('value' => 2, 'label' => 'Two'), + ); $this->assertAllowedValuesInput($string, $array, 'Integer keys are accepted.'); // Check that values can be added and removed. $string = "0|Zero\n1|One"; - $array = array('0' => 'Zero', '1' => 'One'); + $array = array( + array('value' => 0, 'label' => 'Zero'), + array('value' => 1, 'label' => 'One'), + ); $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.'); // Non-integer keys. $this->assertAllowedValuesInput("1.1|One", 'keys must be integers', 'Non integer keys are rejected.'); @@ -82,18 +91,31 @@ function testOptionsAllowedValuesInteger() { // Check that values can be added but values in use cannot be removed. $string = "0|Zero\n1|One\n2|Two"; - $array = array('0' => 'Zero', '1' => 'One', '2' => 'Two'); + $array = array( + array('value' => 0, 'label' => 'Zero'), + array('value' => 1, 'label' => 'One'), + array('value' => 2, 'label' => 'Two'), + ); $this->assertAllowedValuesInput($string, $array, 'Values can be added.'); $string = "0|Zero\n1|One"; - $array = array('0' => 'Zero', '1' => 'One'); + $array = array( + array('value' => 0, 'label' => 'Zero'), + array('value' => 1, 'label' => 'One'), + ); $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); $this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.'); // Delete the node, remove the value. $node->delete(); $string = "0|Zero"; - $array = array('0' => 'Zero'); + $array = array( + array('value' => 0, 'label' => '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"; + $this->assertAllowedValuesInput($string, 'One key can only be used once.', 'Same value cannot be used multiple times.'); } /** @@ -105,15 +127,25 @@ function testOptionsAllowedValuesFloat() { // Flat list of textual values. $string = "Zero\nOne"; - $array = array('0' => 'Zero', '1' => 'One'); + $array = array( + array('value' => (float) 0, 'label' => 'Zero'), + array('value' => (float) 1, 'label' => 'One'), + ); $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.'); // Explicit numeric keys. $string = "0|Zero\n.5|Point five"; - $array = array('0' => 'Zero', '0.5' => 'Point five'); + $array = array( + array('value' => (float) 0, 'label' => 'Zero'), + array('value' => (float) 0.5, 'label' => 'Point five'), + ); $this->assertAllowedValuesInput($string, $array, 'Integer keys are accepted.'); // Check that values can be added and removed. $string = "0|Zero\n.5|Point five\n1.0|One"; - $array = array('0' => 'Zero', '0.5' => 'Point five', '1' => 'One'); + $array = array( + array('value' => (float) 0, 'label' => 'Zero'), + array('value' => (float) 0.5, 'label' => 'Point five'), + array('value' => (float) 1, 'label' => 'One'), + ); $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.'); // Non-numeric keys. $this->assertAllowedValuesInput("abc|abc\n", 'each key must be a valid integer or decimal', 'Non numeric keys are rejected.'); @@ -123,7 +155,7 @@ function testOptionsAllowedValuesFloat() { // Create a node with actual data for the field. $settings = array( 'type' => $this->type, - $this->field_name => array(array('value' => .5)), + $this->field_name => array(array('value' => (float) 0.5)), ); $node = $this->drupalCreateNode($settings); @@ -132,18 +164,35 @@ function testOptionsAllowedValuesFloat() { // Check that values can be added but values in use cannot be removed. $string = "0|Zero\n.5|Point five\n2|Two"; - $array = array('0' => 'Zero', '0.5' => 'Point five', '2' => 'Two'); + $array = array( + array('value' => (float) 0, 'label' => 'Zero'), + array('value' => (float) 0.5, 'label' => 'Point five'), + array('value' => (float) 2, 'label' => 'Two'), + ); $this->assertAllowedValuesInput($string, $array, 'Values can be added.'); $string = "0|Zero\n.5|Point five"; - $array = array('0' => 'Zero', '0.5' => 'Point five'); + $array = array( + array('value' => (float) 0, 'label' => 'Zero'), + array('value' => (float) 0.5, 'label' => 'Point five'), + ); $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); $this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.'); // Delete the node, remove the value. $node->delete(); $string = "0|Zero"; - $array = array('0' => 'Zero'); + $array = array( + array('value' => (float) 0, 'label' => '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"; + $this->assertAllowedValuesInput($string, 'One key can only be used once.', '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"; + $this->assertAllowedValuesInput($string, 'One key can only be used once.', 'Different forms of the same value cannot be used.'); } /** @@ -155,19 +204,31 @@ function testOptionsAllowedValuesText() { // Flat list of textual values. $string = "Zero\nOne"; - $array = array('Zero' => 'Zero', 'One' => 'One'); + $array = array( + array('value' => 'Zero', 'label' => 'Zero'), + array('value' => 'One', 'label' => 'One'), + ); $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.'); // Explicit keys. $string = "zero|Zero\none|One"; - $array = array('zero' => 'Zero', 'one' => 'One'); + $array = array( + array('value' => 'zero', 'label' => 'Zero'), + array('value' => 'one', 'label' => 'One'), + ); $this->assertAllowedValuesInput($string, $array, 'Explicit keys are accepted.'); // Check that values can be added and removed. $string = "zero|Zero\ntwo|Two"; - $array = array('zero' => 'Zero', 'two' => 'Two'); + $array = array( + array('value' => 'zero', 'label' => 'Zero'), + array('value' => 'two', 'label' => 'Two'), + ); $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.'); // Mixed list of keyed and unkeyed values. $string = "zero|Zero\nOne\n"; - $array = array('zero' => 'Zero', 'One' => 'One'); + $array = array( + array('value' => 'zero', 'label' => 'Zero'), + array('value' => 'One', 'label' => 'One'), + ); $this->assertAllowedValuesInput($string, $array, 'Mixed lists are accepted.'); // Overly long keys. $this->assertAllowedValuesInput("zero|Zero\n" . $this->randomName(256) . "|One", 'each key must be a string at most 255 characters long', 'Overly long keys are rejected.'); @@ -182,23 +243,48 @@ function testOptionsAllowedValuesText() { // Check that flat lists of values are still accepted once the field has // data. $string = "Zero\nOne"; - $array = array('Zero' => 'Zero', 'One' => 'One'); + $array = array( + array('value' => 'Zero', 'label' => 'Zero'), + array('value' => 'One', 'label' => 'One'), + ); $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are still accepted once the field has data.'); // Check that values can be added but values in use cannot be removed. $string = "Zero\nOne\nTwo"; - $array = array('Zero' => 'Zero', 'One' => 'One', 'Two' => 'Two'); + $array = array( + array('value' => 'Zero', 'label' => 'Zero'), + array('value' => 'One', 'label' => 'One'), + array('value' => 'Two', 'label' => 'Two'), + ); $this->assertAllowedValuesInput($string, $array, 'Values can be added.'); $string = "Zero\nOne"; - $array = array('Zero' => 'Zero', 'One' => 'One'); + $array = array( + array('value' => 'Zero', 'label' => 'Zero'), + array('value' => 'One', 'label' => 'One'), + ); $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); $this->assertAllowedValuesInput("Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.'); // Delete the node, remove the value. $node->delete(); $string = "Zero"; - $array = array('Zero' => 'Zero'); + $array = array( + array('value' => 'Zero', 'label' => 'Zero'), + ); $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.'); + + // Check that string values with dots can be used. + $node->delete(); + $string = "Zero\nexample.com|Example"; + $array = array( + array('value' => 'Zero', 'label' => 'Zero'), + array('value' => 'example.com', 'label' => 'Example'), + ); + $this->assertAllowedValuesInput($string, $array, 'String value with dot is supported.'); + + // Check that the same key can only be used once. + $string = "Zero\nZero"; + $this->assertAllowedValuesInput($string, 'One key can only be used once.', 'Same value cannot be used multiple times.'); } /** @@ -210,8 +296,15 @@ function testOptionsTrimmedValuesText() { // Explicit keys. $string = "zero |Zero\none | One"; - $array = array('zero' => 'Zero', 'one' => 'One'); + $array = array( + array('value' => 'zero', 'label' => 'Zero'), + array('value' => 'one', 'label' => 'One'), + ); $this->assertAllowedValuesInput($string, $array, 'Explicit keys are accepted and trimmed.'); + + // Check that the same key can only be used once. + $string = " Zero \n Zero "; + $this->assertAllowedValuesInput($string, 'One key can only be used once.', 'Same value cannot be used multiple times.'); } /** diff --git a/core/modules/options/src/Tests/OptionsFieldUnitTestBase.php b/core/modules/options/src/Tests/OptionsFieldUnitTestBase.php index 272e026..2c97d2e 100644 --- a/core/modules/options/src/Tests/OptionsFieldUnitTestBase.php +++ b/core/modules/options/src/Tests/OptionsFieldUnitTestBase.php @@ -63,7 +63,11 @@ public function setUp() { 'type' => 'list_integer', 'cardinality' => 1, 'settings' => array( - 'allowed_values' => array(1 => 'One', 2 => 'Two', 3 => 'Three'), + 'allowed_values' => array( + array('value' => 1, 'label' => 'One'), + array('value' => 2, 'label' => 'Two'), + array('value' => 3, 'label' => 'Three') + ), ), ); $this->fieldStorage = entity_create('field_storage_config', $this->fieldStorageDefinition); diff --git a/core/modules/options/src/Tests/OptionsWidgetsTest.php b/core/modules/options/src/Tests/OptionsWidgetsTest.php index d124840..8ba1c7a 100644 --- a/core/modules/options/src/Tests/OptionsWidgetsTest.php +++ b/core/modules/options/src/Tests/OptionsWidgetsTest.php @@ -57,12 +57,12 @@ function setUp() { 'settings' => array( 'allowed_values' => array( // Make sure that 0 works as an option. - 0 => 'Zero', - 1 => 'One', + array('value' => 0, 'label' => 'Zero'), + array('value' => 1, 'label' => 'One'), // Make sure that option text is properly sanitized. - 2 => 'Some & unescaped markup', + array('value' => 2, 'label' => 'Some & unescaped markup'), // Make sure that HTML entities in option text are not double-encoded. - 3 => 'Some HTML encoded markup with < & >', + array('value' => 3, 'label' => 'Some HTML encoded markup with < & >'), ), ), )); @@ -77,10 +77,10 @@ function setUp() { 'settings' => array( 'allowed_values' => array( // Make sure that 0 works as an option. - 0 => 'Zero', - 1 => 'One', + array('value' => 0, 'label' => 'Zero'), + array('value' => 1, 'label' => 'One'), // Make sure that option text is properly sanitized. - 2 => 'Some & unescaped markup', + array('value' => 2, 'label' => 'Some & unescaped markup'), ), ), )); @@ -140,7 +140,7 @@ function testRadioButtons() { $this->assertFieldValues($entity_init, 'card_1', array()); // Check that required radios with one option is auto-selected. - $this->card_1->settings['allowed_values'] = array(99 => 'Only allowed value'); + $this->card_1->settings['allowed_values'] = array(array('value' => 99, 'label' => 'Only allowed value')); $this->card_1->save(); $instance->required = TRUE; $instance->save(); @@ -229,7 +229,7 @@ function testCheckBoxes() { $this->assertFieldValues($entity_init, 'card_2', array()); // Required checkbox with one option is auto-selected. - $this->card_2->settings['allowed_values'] = array(99 => 'Only allowed value'); + $this->card_2->settings['allowed_values'] = array(array('value' => 99, 'label' => 'Only allowed value')); $this->card_2->save(); $instance->required = TRUE; $instance->save(); diff --git a/core/modules/options/tests/options_test.module b/core/modules/options/tests/options_test.module index 1416fa0..ba56132 100644 --- a/core/modules/options/tests/options_test.module +++ b/core/modules/options/tests/options_test.module @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\options\Plugin\Field\FieldType\ListItemBase; /** * Allowed values callback. @@ -22,7 +23,7 @@ function options_test_allowed_values_callback(FieldDefinitionInterface $field_de ), ); - return $values; + return ListItemBase::structureAllowedValues($values); } /** @@ -37,5 +38,5 @@ function options_test_dynamic_values_callback(FieldDefinitionInterface $field_de $entity->bundle(), ); // We need the values of the entity as keys. - return array_combine($values, $values); + return ListItemBase::structureAllowedValues(array_combine($values, $values)); }