Unverified Commit 8cd35dbe 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

(cherry picked from commit c9dd6558)
parent afbdd37f
......@@ -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