Commit 54c2e3d5 authored by alexpott's avatar alexpott

Issue #2002152 by fago, amateescu: Implement entity validation based on symfony validator.

parent 5b5d5351
......@@ -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);
}
}
......@@ -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 \Drupal::typedData()
......
......@@ -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,19 @@ public function onChange($property_name) {
// updated property object.
unset($this->values[$property_name]);
}
}
\ No newline at end of file
/**
* {@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;
}
}
......@@ -194,4 +194,19 @@ public function defaultAccess($operation = 'view', UserInterface $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;
}
}
......@@ -303,28 +303,6 @@ public function getValidationConstraintManager() {
return $this->constraintManager;
}
/**
* 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.
*
......@@ -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;
}
......
......@@ -55,6 +55,28 @@ public function __construct(\Traversable $namespaces) {
$this->factory = new DefaultFactory($this);
}
/**
* 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.
*
......
<?php
/**
* @file
* Contains \Drupal\Core\Validation\Plugin\Validation\Constraint\CollectionConstraint.
*/
namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
use Drupal\Component\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Symfony\Component\Validator\Constraint;
/**
* Complex data constraint.
*
* Validates properties of complex data structures.
*
* @Plugin(
* id = "ComplexData",
* label = @Translation("Complex data", context = "Validation")
* )
*/
class ComplexDataConstraint extends Constraint {
/**
* An array of constraints for contained properties, keyed by property name.
*
* @var array
*/
public $properties;
/**
* {@inheritdoc}
*/
public function __construct($options = NULL) {
// Allow skipping the 'properties' key in the options.
if (is_array($options) && !array_key_exists('properties', $options)) {
$options = array('properties' => $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');
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Validation\Plugin\Validation\Constraint\ComplexDataConstraintValidator.
*/
namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
use Drupal\Core\TypedData\ComplexDataInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* Validates complex data.
*/
class ComplexDataConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
if (!isset($value)) {
return;
}
if (!$value instanceof ComplexDataInterface) {
throw new UnexpectedTypeException($value, 'ComplexData');
}
$group = $this->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);
}
}
}
<?php
/**
* @file
* Contains \Drupal\system\Tests\Entity\EntityValidationTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Field\FieldInterface;
use Drupal\Core\Entity\Field\FieldItemInterface;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Tests Entity API base functionality.
*/
class EntityValidationTest extends EntityUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('filter', 'text');
public static function getInfo() {
return array(
'name' => 'Entity Validation API',
'description' => 'Tests the Entity Validation API',
'group' => 'Entity API',
);
}
/**
* {@inheritdoc}
*/
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.
*
* @param string $entity_type
* An entity type.
*
* @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);
$violations = $test_entity->validate();
$this->assertEqual($violations->count(), 1, 'Validation failed.');
$this->assertEqual($violations[0]->getMessage(), t('This value is too long. It should have %limit characters or less.', array('%limit' => '128')));
$test_entity = clone $entity;
$test_entity->langcode->value = $this->randomString(13);
$violations = $test_entity->validate();
$this->assertEqual($violations->count(), 1, 'Validation failed.');
$this->assertEqual($violations[0]->getMessage(), t('This value is too long. It should have %limit characters or less.', array('%limit' => '12')));
$test_entity = clone $entity;
$test_entity->type->value = NULL;
$violations = $test_entity->validate();
$this->assertEqual($violations->count(), 1, 'Validation failed.');
$this->assertEqual($violations[0]->getMessage(), t('This value should not be null.'));
$test_entity = clone $entity;
$test_entity->name->value = $this->randomString(33);
$violations = $test_entity->validate();
$this->assertEqual($violations->count(), 1, 'Validation failed.');
$this->assertEqual($violations[0]->getMessage(), t('This value is too long. It should have %limit characters or less.', array('%limit' => '32')));
// Make sure the information provided by a violation is correct.
$violation = $violations[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.');
}
}
......@@ -2280,6 +2280,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'),
......
......@@ -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'),
......
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