diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index 6a93a59..bbc4507 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -527,4 +527,12 @@ public function label($langcode = NULL) { } return $label; } + + /** + * {@inheritdoc} + */ + public function validate() { + // @todo: Add the typed data manager as proper dependency. + return \Drupal::typedData()->getValidator()->validate($this); + } } diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php index 377a002..e189248 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php @@ -153,6 +153,14 @@ public function save(EntityInterface $entity); * - translatable: Whether the field is translatable. Defaults to FALSE. * - configurable: A boolean indicating whether the field is configurable * via field.module. Defaults to FALSE. + * - property_constraints: An array of constraint arrays applying to the + * field item properties, keyed by property name. E.g. the following + * validates the value property to have a maximum length of 128: + * @code + * array( + * 'value' => array('Length' => array('max' => 128)), + * ) + * @endcode * * @see Drupal\Core\TypedData\TypedDataManager::create() * @see typed_data() diff --git a/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php b/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php index 2c3feef..fa28bc6 100644 --- a/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php +++ b/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php @@ -91,6 +91,7 @@ public function set($property_name, $value, $notify = TRUE) { // value that needs to be updated. if (isset($this->properties[$property_name])) { $this->properties[$property_name]->setValue($value, FALSE); + unset($this->values[$property_name]); } // Allow setting plain values for not-defined properties also. else { @@ -136,4 +137,18 @@ public function onChange($property_name) { // updated property object. unset($this->values[$property_name]); } + + /** + * {@inheritdoc} + */ + public function getConstraints() { + $constraints = parent::getConstraints(); + // If property constraints are present add in a ComplexData constraint for + // applying them. + if (!empty($this->definition['property_constraints'])) { + $constraints[] = \Drupal::typedData()->getValidationConstraintManager() + ->create('ComplexData', $this->definition['property_constraints']); + } + return $constraints; + } } \ No newline at end of file diff --git a/core/lib/Drupal/Core/Entity/Field/Type/Field.php b/core/lib/Drupal/Core/Entity/Field/Type/Field.php index 4829bb7..469bab7 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/Field.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/Field.php @@ -194,4 +194,19 @@ public function defaultAccess($operation = 'view', User $account = NULL) { // Grant access per default. return TRUE; } + + /** + * {@inheritdoc} + */ + public function getConstraints() { + // Constraints usually apply to the field item, but required does make + // sense on the field only. So we special-case it to apply to the field for + // now. + // @todo: Separate list and list item definitions to separate constraints. + $constraints = array(); + if (!empty($this->definition['required'])) { + $constraints[] = \Drupal::typedData()->getValidationConstraintManager()->create('NotNull', array()); + } + return $constraints; + } } diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php index 195e32d..4bfc5c9 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -304,28 +304,6 @@ public function getValidationConstraintManager() { } /** - * Creates a validation constraint plugin. - * - * @param string $name - * The name or plugin id of the constraint. - * @param mixed $options - * The options to pass to the constraint class. Required and supported - * options depend on the constraint class. - * - * @return \Symfony\Component\Validator\Constraint - * A validation constraint plugin. - */ - public function createValidationConstraint($name, $options) { - if (!is_array($options)) { - // Plugins need an array as configuration, so make sure we have one. - // The constraint classes support passing the options as part of the - // 'value' key also. - $options = array('value' => $options); - } - return $this->getValidationConstraintManager()->createInstance($name, $options); - } - - /** * Gets configured constraints from a data definition. * * Any constraints defined for the data type, i.e. below the 'constraint' key @@ -365,29 +343,28 @@ public function createValidationConstraint($name, $options) { */ public function getConstraints($definition) { $constraints = array(); - // @todo: Figure out how to handle nested constraint structures as - // collections. + $validation_manager = $this->getValidationConstraintManager(); + $type_definition = $this->getDefinition($definition['type']); // Auto-generate a constraint for the primitive type if we have a mapping. if (isset($type_definition['primitive type'])) { - $constraints[] = $this->getValidationConstraintManager()-> - createInstance('PrimitiveType', array('type' => $type_definition['primitive type'])); + $constraints[] = $validation_manager->create('PrimitiveType', array('type' => $type_definition['primitive type'])); } // Add in constraints specified by the data type. if (isset($type_definition['constraints'])) { foreach ($type_definition['constraints'] as $name => $options) { - $constraints[] = $this->createValidationConstraint($name, $options); + $constraints[] = $validation_manager->create($name, $options); } } // Add any constraints specified as part of the data definition. if (isset($definition['constraints'])) { foreach ($definition['constraints'] as $name => $options) { - $constraints[] = $this->createValidationConstraint($name, $options); + $constraints[] = $validation_manager->create($name, $options); } } // Add the NotNull constraint for required data. if (!empty($definition['required']) && empty($definition['constraints']['NotNull'])) { - $constraints[] = $this->createValidationConstraint('NotNull', array()); + $constraints[] = $validation_manager->create('NotNull', array()); } return $constraints; } diff --git a/core/lib/Drupal/Core/Validation/ConstraintManager.php b/core/lib/Drupal/Core/Validation/ConstraintManager.php index da84d55..859cfc1 100644 --- a/core/lib/Drupal/Core/Validation/ConstraintManager.php +++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php @@ -56,6 +56,28 @@ public function __construct(\Traversable $namespaces) { } /** + * Creates a validation constraint. + * + * @param string $name + * The name or plugin id of the constraint. + * @param mixed $options + * The options to pass to the constraint class. Required and supported + * options depend on the constraint class. + * + * @return \Symfony\Component\Validator\Constraint + * A validation constraint plugin. + */ + public function create($name, $options) { + if (!is_array($options)) { + // Plugins need an array as configuration, so make sure we have one. + // The constraint classes support passing the options as part of the + // 'value' key also. + $options = array('value' => $options); + } + return $this->createInstance($name, $options); + } + + /** * Callback for registering definitions for constraints shipped with Symfony. * * @see ConstraintManager::__construct() diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraint.php new file mode 100644 index 0000000..5719051 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraint.php @@ -0,0 +1,67 @@ + $options); + } + parent::__construct($options); + $constraint_manager = \Drupal::service('validation.constraint'); + + // Instantiate constraint objects for array definitions. + foreach ($this->properties as &$constraints) { + foreach ($constraints as $id => $options) { + if (!is_object($options)) { + $constraints[$id] = $constraint_manager->create($id, $options); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function getDefaultOption() { + return 'properties'; + } + + /** + * {@inheritdoc} + */ + public function getRequiredOptions() { + return array('properties'); + } +} diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php new file mode 100644 index 0000000..663b570 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php @@ -0,0 +1,47 @@ +context->getGroup(); + + foreach ($constraint->properties as $name => $constraints) { + $property = $value->get($name); + $is_container = $property instanceof ComplexDataInterface || $property instanceof ListInterface; + if (!$is_container) { + $property = $property->getValue(); + } + elseif ($property->isEmpty()) { + // @see \Drupal\Core\TypedData\Validation\PropertyContainerMetadata::accept(); + $property = NULL; + } + $this->context->validateValue($property, $constraints, $name, $group); + } + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php new file mode 100644 index 0000000..df4d8f4 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php @@ -0,0 +1,126 @@ + 'Entity Validation API', + 'description' => 'Tests the Entity Validation API', + 'group' => 'Entity API', + ); + } + + public function setUp() { + parent::setUp(); + $this->installSchema('user', array('users_roles', 'users_data')); + $this->installSchema('entity_test', array( + 'entity_test_mul', + 'entity_test_mul_property_data', + 'entity_test_rev', + 'entity_test_rev_revision', + 'entity_test_mulrev', + 'entity_test_mulrev_property_data', + 'entity_test_mulrev_property_revision' + )); + + // Create the test field. + entity_test_install(); + + // Install required default configuration for filter module. + $this->installConfig(array('system', 'filter')); + } + + /** + * Creates a test entity. + * + * @return \Drupal\Core\Entity\EntityInterface + * The created test entity. + */ + protected function createTestEntity($entity_type) { + $this->entity_name = $this->randomName(); + $this->entity_user = $this->createUser(); + $this->entity_field_text = $this->randomName(); + + // Pass in the value of the name field when creating. With the user + // field we test setting a field after creation. + $entity = entity_create($entity_type, array()); + $entity->user_id->target_id = $this->entity_user->uid; + $entity->name->value = $this->entity_name; + + // Set a value for the test field. + $entity->field_test_text->value = $this->entity_field_text; + + return $entity; + } + + /** + * Tests validating test entity types. + */ + public function testValidation() { + // All entity variations have to have the same results. + foreach (entity_test_entity_types() as $entity_type) { + $this->checkValidation($entity_type); + } + } + + /** + * Executes the validation test set for a defined entity type. + * + * @param string $entity_type + * The entity type to run the tests with. + */ + protected function checkValidation($entity_type) { + $entity = $this->createTestEntity($entity_type); + $violations = $entity->validate(); + $this->assertEqual($violations->count(), 0, 'Validation passes.'); + + // Test triggering a fail for each of the constraints specified. + $test_entity = clone $entity; + $test_entity->uuid->value = $this->randomString(129); + $this->assertEqual($test_entity->validate()->count(), 1, 'Validation failed.'); + + $test_entity = clone $entity; + $test_entity->langcode->value = $this->randomString(13); + $this->assertEqual($test_entity->validate()->count(), 1, 'Validation failed.'); + + $test_entity = clone $entity; + $test_entity->type->value = NULL; + $this->assertEqual($test_entity->validate()->count(), 1, 'Validation failed.'); + + $test_entity = clone $entity; + $test_entity->name->value = $this->randomString(33); + $this->assertEqual($test_entity->validate()->count(), 1, 'Validation failed.'); + + // Make sure the violation is correct. + $violations = $test_entity->validate(); + $violation = $violations->get(0); + $this->assertEqual($violation->getRoot(), $test_entity, 'Violation root is entity.'); + $this->assertEqual($violation->getPropertyPath(), 'name.0.value', 'Violation property path is correct.'); + + $this->assertEqual($violation->getInvalidValue(), $test_entity->name->value, 'Violation contains invalid value.'); + $this->assertEqual($violation->getMessage(), t('This value is too long. It should have %limit characters or less.', array('%limit' => '32'))); + } + +} diff --git a/core/modules/system/system.module b/core/modules/system/system.module index bf0977b..bba9ddf 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2316,6 +2316,11 @@ function system_data_type_info() { 'description' => t('An entity field referencing a language.'), 'class' => '\Drupal\Core\Entity\Field\Type\LanguageItem', 'list class' => '\Drupal\Core\Entity\Field\Type\Field', + 'constraints' => array( + 'ComplexData' => array( + 'value' => array('Length' => array('max' => 12)), + ), + ), ), 'entity_reference_field' => array( 'label' => t('Entity reference field item'), diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php index 8c5e23f..1d2b879 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php @@ -41,6 +41,9 @@ public function baseFieldDefinitions() { 'label' => t('UUID'), 'description' => t('The UUID of the test entity.'), 'type' => 'string_field', + 'property_constraints' => array( + 'value' => array('Length' => array('max' => 128)), + ), ); $fields['langcode'] = array( 'label' => t('Language code'), @@ -52,11 +55,16 @@ public function baseFieldDefinitions() { 'description' => t('The name of the test entity.'), 'type' => 'string_field', 'translatable' => TRUE, + 'property_constraints' => array( + 'value' => array('Length' => array('max' => 32)), + ), ); $fields['type'] = array( 'label' => t('Type'), 'description' => t('The bundle of the test entity.'), 'type' => 'string_field', + 'required' => TRUE, + // @todo: Add allowed values validation. ); $fields['user_id'] = array( 'label' => t('User ID'),