Unverified Commit c9dd6558 authored by larowlan's avatar larowlan
Browse files

Issue #2937542 by alexpott, mikelutz, larowlan, dawehner, Mile23, catch, Gábor...

Issue #2937542 by alexpott, mikelutz, larowlan, dawehner, Mile23, catch, Gábor Hojtsy, Chi: Not setting the strict option of the Choice constraint to true is deprecated since Symfony 3.4 and will throw an exception in 4.0
parent cb04f91c
......@@ -16,6 +16,7 @@
*/
class AllowedValuesConstraint extends Choice {
public $strict = TRUE;
public $minMessage = 'You must select at least %limit choice.|You must select at least %limit choices.';
public $maxMessage = 'You must select at most %limit choice.|You must select at most %limit choices.';
......
......@@ -6,10 +6,12 @@
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\OptionsProviderInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\PrimitiveInterface;
use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\ChoiceValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
/**
* Validates the AllowedValues constraint.
......@@ -47,6 +49,7 @@ public function __construct(AccountInterface $current_user) {
*/
public function validate($value, Constraint $constraint) {
$typed_data = $this->getTypedData();
if ($typed_data instanceof OptionsProviderInterface) {
$allowed_values = $typed_data->getSettableValues($this->currentUser);
$constraint->choices = $allowed_values;
......@@ -57,7 +60,8 @@ public function validate($value, Constraint $constraint) {
if (!isset($name)) {
throw new \LogicException('Cannot validate allowed values for complex data without a main property.');
}
$value = $typed_data->get($name)->getValue();
$typed_data = $typed_data->get($name);
$value = $typed_data->getValue();
}
}
......@@ -69,6 +73,36 @@ public function validate($value, Constraint $constraint) {
return;
}
// Get the value with the proper datatype in order to make strict
// comparisons using in_array().
if (!($typed_data instanceof PrimitiveInterface)) {
throw new \LogicException('The data type must be a PrimitiveInterface at this point.');
}
$value = $typed_data->getCastedValue();
// In a better world where typed data just returns typed values, we could
// set a constraint callback to use the OptionsProviderInterface.
// This is not possible right now though because we do the typecasting
// further down.
if ($constraint->callback) {
if (!\is_callable($choices = [$this->context->getObject(), $constraint->callback])
&& !\is_callable($choices = [$this->context->getClassName(), $constraint->callback])
&& !\is_callable($choices = $constraint->callback)
) {
throw new ConstraintDefinitionException('The AllowedValuesConstraint constraint expects a valid callback');
}
$allowed_values = \call_user_func($choices);
$constraint->choices = $allowed_values;
// parent::validate() does not need to invoke the callback again.
$constraint->callback = NULL;
}
// Force the choices to be the same type as the value.
$type = gettype($value);
foreach ($constraint->choices as &$choice) {
settype($choice, $type);
}
parent::validate($value, $constraint);
}
......
......@@ -35,6 +35,13 @@ class OptionsWidgetsTest extends FieldTestBase {
*/
protected $card2;
/**
* A field storage with float values to use in this test class.
*
* @var \Drupal\field\Entity\FieldStorageConfig
*/
protected $float;
protected function setUp() {
parent::setUp();
......@@ -76,6 +83,22 @@ protected function setUp() {
]);
$this->card2->save();
// Field storage with list of float values.
$this->float = FieldStorageConfig::create([
'field_name' => 'float',
'entity_type' => 'entity_test',
'type' => 'list_float',
'cardinality' => 1,
'settings' => [
'allowed_values' => [
'0.0' => '0.0',
'1.5' => '1.5',
'2.0' => '2.0',
],
],
]);
$this->float->save();
// Create a web user.
$this->drupalLogin($this->drupalCreateUser(['view test entity', 'administer entity_test content']));
}
......@@ -447,6 +470,53 @@ public function testSelectListMultiple() {
$this->assertFieldValues($entity_init, 'card_2', []);
}
/**
* Tests the 'options_select' widget (float values).
*/
public function testSelectListFloat() {
// Create an instance of the 'float value' field.
$field = FieldConfig::create([
'field_storage' => $this->float,
'bundle' => 'entity_test',
'required' => TRUE,
]);
$field->save();
$this->container
->get('entity_type.manager')
->getStorage('entity_form_display')
->load('entity_test.entity_test.default')
->setComponent($this->float->getName(), ['type' => 'options_select'])
->save();
// Create an entity.
$entity = EntityTest::create([
'user_id' => 1,
'name' => $this->randomMachineName(),
]);
$entity->save();
// Display form.
$this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
// With no field data, nothing is selected.
$this->assertFalse($this->assertSession()->optionExists('float', 0)->isSelected());
$this->assertFalse($this->assertSession()->optionExists('float', 1.5)->isSelected());
$this->assertFalse($this->assertSession()->optionExists('float', 2)->isSelected());
// Submit form.
$edit = ['float' => 1.5];
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertFieldValues($entity, 'float', [1.5]);
// Display form: check that the right options are selected.
$this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
$this->assertFalse($this->assertSession()->optionExists('float', 0)->isSelected());
$this->assertTrue($this->assertSession()->optionExists('float', 1.5)->isSelected());
$this->assertFalse($this->assertSession()->optionExists('float', 2)->isSelected());
}
/**
* Tests the 'options_select' and 'options_button' widget for empty value.
*/
......
......@@ -4,6 +4,7 @@
use Drupal\Core\TypedData\DataDefinition;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
/**
* Tests AllowedValues validation constraint with both valid and invalid values.
......@@ -49,6 +50,66 @@ public function testValidation() {
$this->assertEqual($violation->getMessage(), t('The value you selected is not a valid choice.'), 'The message for invalid value is correct.');
$this->assertEqual($violation->getRoot(), $typed_data, 'Violation root is correct.');
$this->assertEqual($violation->getInvalidValue(), 4, 'The invalid value is set correctly in the violation.');
// Test the validation when a value of an incorrect type is passed.
$typed_data = $this->typedData->create($definition, '1');
$violations = $typed_data->validate();
$this->assertEquals(0, $violations->count(), 'Value is coerced to the correct type and is valid.');
}
/**
* Tests the AllowedValuesConstraintValidator with callbacks.
*/
public function testValidationCallback() {
// Create a definition that specifies some AllowedValues and a callback.
// This tests that callbacks have a higher priority than a supplied list of
// values and can be used to coerce the value to the correct type.
$definition = DataDefinition::create('string')
->addConstraint('AllowedValues', ['choices' => [1, 2, 3], 'callback' => [static::class, 'allowedValueCallback']]);
$typed_data = $this->typedData->create($definition, 'a');
$violations = $typed_data->validate();
$this->assertEquals(0, $violations->count(), 'Validation passed for correct value.');
$typed_data = $this->typedData->create($definition, 1);
$violations = $typed_data->validate();
$this->assertEquals(0, $violations->count(), 'Validation passed for value that will be cast to the correct type.');
$typed_data = $this->typedData->create($definition, 2);
$violations = $typed_data->validate();
$this->assertEquals(1, $violations->count(), 'Validation failed for incorrect value.');
$typed_data = $this->typedData->create($definition, 'd');
$violations = $typed_data->validate();
$this->assertEquals(1, $violations->count(), 'Validation failed for incorrect value.');
$typed_data = $this->typedData->create($definition, 0);
$violations = $typed_data->validate();
$this->assertEquals(1, $violations->count(), 'Validation failed for incorrect value.');
}
/**
* An AllowedValueConstraint callback.
*
* @return string[]
* A list of allowed values.
*/
public static function allowedValueCallback() {
return ['a', 'b', 'c', '1'];
}
/**
* Tests the AllowedValuesConstraintValidator with an invalid callback.
*/
public function testValidationCallbackException() {
// Create a definition that specifies some AllowedValues and a callback.
// This tests that callbacks have a higher priority than a supplied list of
// values and can be used to coerce the value to the correct type.
$definition = DataDefinition::create('string')
->addConstraint('AllowedValues', ['choices' => [1, 2, 3], 'callback' => [static::class, 'doesNotExist']]);
$typed_data = $this->typedData->create($definition, 1);
$this->setExpectedException(ConstraintDefinitionException::class, 'The AllowedValuesConstraint constraint expects a valid callback');
$typed_data->validate();
}
}
......@@ -126,7 +126,6 @@ public static function getSkippedDeprecations() {
// is a Windows only deprecation. Remove when core no longer uses
// WinCacheClassLoader in \Drupal\Core\DrupalKernel::initializeSettings().
'The Symfony\Component\ClassLoader\WinCacheClassLoader class is deprecated since Symfony 3.3 and will be removed in 4.0. Use `composer install --apcu-autoloader` instead.',
'Not setting the strict option of the Choice constraint to true is deprecated since Symfony 3.4 and will throw an exception in 4.0.',
];
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment