diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 71ef37b..538290e 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -103,7 +103,10 @@ public function build(ContainerBuilder $container) { ->setFactoryClass('Drupal\Core\Database\Database') ->setFactoryMethod('getConnection') ->addArgument('slave'); - $container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager'); + $container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager') + ->addMethodCall('setValidationConstraintManager', array(new Reference('validation.constraint', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))); + $container->register('validation.constraint', 'Drupal\Core\Validation\ConstraintManager'); + // Add the user's storage for temporary, non-cache data. $container->register('lock', 'Drupal\Core\Lock\DatabaseLockBackend'); $container->register('user.tempstore', 'Drupal\user\TempStoreFactory') diff --git a/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php b/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php index 5c71aa3..9b51629 100644 --- a/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php +++ b/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php @@ -121,13 +121,6 @@ public function getString() { } /** - * Implements TypedDataInterface::validate(). - */ - public function validate() { - // @todo implement - } - - /** * Implements ComplexDataInterface::get(). */ public function get($property_name) { diff --git a/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php b/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php index a60e65e..261bd92 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php @@ -39,6 +39,9 @@ public function getPropertyDefinitions() { // @todo: Lookup the entity type's ID data type and use it here. 'type' => 'integer', 'label' => t('Entity ID'), + 'constraints' => array( + 'Min' => array('limit' => 0), + ), ); self::$propertyDefinitions[$entity_type]['entity'] = array( 'type' => 'entity', diff --git a/core/lib/Drupal/Core/Entity/Field/Type/EntityTranslation.php b/core/lib/Drupal/Core/Entity/Field/Type/EntityTranslation.php index 280fca9..4a08987 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/EntityTranslation.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/EntityTranslation.php @@ -237,11 +237,4 @@ public function access($operation = 'view', \Drupal\user\Plugin\Core\Entity\User $method = $operation . 'Access'; return entity_access_controller($this->parent->entityType())->$method($this->parent, $this->langcode, $account); } - - /** - * Implements TypedDataInterface::validate(). - */ - public function validate($value = NULL) { - // @todo implement - } } diff --git a/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php b/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php index 97449e7..5607ec4 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php @@ -125,13 +125,6 @@ public function getString() { } /** - * Implements TypedDataInterface::validate(). - */ - public function validate($value = NULL) { - // TODO: Implement validate() method. - } - - /** * Implements IteratorAggregate::getIterator(). */ public function getIterator() { diff --git a/core/lib/Drupal/Core/Entity/Field/Type/Field.php b/core/lib/Drupal/Core/Entity/Field/Type/Field.php index cc5951c..9d2448a 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/Field.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/Field.php @@ -114,10 +114,11 @@ public function getString() { } /** - * Implements TypedDataInterface::validate(). + * Implements TypedDataInterface::getConstraints(). */ - public function validate() { - // @todo implement + public function getConstraints() { + // Apply the constraints to the list items only. + return array(); } /** diff --git a/core/lib/Drupal/Core/Entity/Field/Type/IntegerItem.php b/core/lib/Drupal/Core/Entity/Field/Type/IntegerItem.php index 1f4b4e6..728a65f 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/IntegerItem.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/IntegerItem.php @@ -32,6 +32,11 @@ public function getPropertyDefinitions() { self::$propertyDefinitions['value'] = array( 'type' => 'integer', 'label' => t('Integer value'), + // @todo: Remove this in favor of a 'Type' constraint and move it to the + // integer type definition. + 'constraints' => array( + 'Min' => array('limit' => 5), + ), ); } return self::$propertyDefinitions; diff --git a/core/lib/Drupal/Core/Plugin/Validation/Constraint/EntityTypeConstraint.php b/core/lib/Drupal/Core/Plugin/Validation/Constraint/EntityTypeConstraint.php new file mode 100644 index 0000000..ca46ee8 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Validation/Constraint/EntityTypeConstraint.php @@ -0,0 +1,38 @@ +value = isset($value) ? (bool) $value : $value; } - - /** - * Implements TypedDataInterface::validate(). - */ - public function validate() { - // TODO: Implement validate() method. - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Date.php b/core/lib/Drupal/Core/TypedData/Type/Date.php index 3c0f3a9..9822b37 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Date.php +++ b/core/lib/Drupal/Core/TypedData/Type/Date.php @@ -52,11 +52,4 @@ public function setValue($value) { public function getString() { return (string) $this->getValue(); } - - /** - * Implements TypedDataInterface::validate(). - */ - public function validate() { - // TODO: Implement validate() method. - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Duration.php b/core/lib/Drupal/Core/TypedData/Type/Duration.php index 9fd8ceb..68093fd 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Duration.php +++ b/core/lib/Drupal/Core/TypedData/Type/Duration.php @@ -58,11 +58,4 @@ public function getString() { // DateInterval::__construct() and setValue(). return (string) $this->getValue()->format('%rP%yY%mM%dDT%hH%mM%sS'); } - - /** - * Implements TypedDataInterface::validate(). - */ - public function validate() { - // TODO: Implement validate() method. - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Float.php b/core/lib/Drupal/Core/TypedData/Type/Float.php index 798499c..ec77fa8 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Float.php +++ b/core/lib/Drupal/Core/TypedData/Type/Float.php @@ -30,11 +30,4 @@ class Float extends TypedData implements TypedDataInterface { public function setValue($value) { $this->value = isset($value) ? (float) $value : $value; } - - /** - * Implements TypedDataInterface::validate(). - */ - public function validate() { - // TODO: Implement validate() method. - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Integer.php b/core/lib/Drupal/Core/TypedData/Type/Integer.php index 4e9b59a..ec21349 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Integer.php +++ b/core/lib/Drupal/Core/TypedData/Type/Integer.php @@ -30,11 +30,4 @@ class Integer extends TypedData implements TypedDataInterface { public function setValue($value) { $this->value = isset($value) ? (int) $value : $value; } - - /** - * Implements TypedDataInterface::validate(). - */ - public function validate() { - // TODO: Implement validate() method. - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Language.php b/core/lib/Drupal/Core/TypedData/Type/Language.php index 67f4df8..483bbc9 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Language.php +++ b/core/lib/Drupal/Core/TypedData/Type/Language.php @@ -124,11 +124,4 @@ public function getString() { $language = $this->getValue(); return $language ? $language->name : ''; } - - /** - * Implements TypedDataInterface::validate(). - */ - public function validate() { - // TODO: Implement validate() method. - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/String.php b/core/lib/Drupal/Core/TypedData/Type/String.php index fef3248..b15042b 100644 --- a/core/lib/Drupal/Core/TypedData/Type/String.php +++ b/core/lib/Drupal/Core/TypedData/Type/String.php @@ -30,11 +30,4 @@ class String extends TypedData implements TypedDataInterface { public function setValue($value) { $this->value = isset($value) ? (string) $value : $value; } - - /** - * Implements TypedDataInterface::validate(). - */ - public function validate() { - // TODO: Implement validate() method. - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/TypedData.php b/core/lib/Drupal/Core/TypedData/Type/TypedData.php index 1e70c53..c3ab452 100644 --- a/core/lib/Drupal/Core/TypedData/Type/TypedData.php +++ b/core/lib/Drupal/Core/TypedData/Type/TypedData.php @@ -70,4 +70,28 @@ public function setValue($value) { public function getString() { return (string) $this->getValue(); } + + /** + * Implements TypedDataInterface::getConstraints(). + */ + public function getConstraints() { + $constraints = array(); + if (isset($this->definition['constraints'])) { + foreach ($this->definition['constraints'] as $name => $options) { + // @todo: Add the typed data manager as proper dependency. + // @todo: Figure out how to handle nested constraint structures as + // collections. + $constraints[] = typed_data()->getValidationConstraintManager()->createInstance($name, $options); + } + } + return $constraints; + } + + /** + * Implements TypedDataInterface::validate(). + */ + public function validate() { + // @todo: Add the typed data manager as proper dependency. + return typed_data()->getValidator()->validate($this); + } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Uri.php b/core/lib/Drupal/Core/TypedData/Type/Uri.php index 010fa03..354eed5 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Uri.php +++ b/core/lib/Drupal/Core/TypedData/Type/Uri.php @@ -29,11 +29,4 @@ class Uri extends TypedData implements TypedDataInterface { public function setValue($value) { $this->value = isset($value) ? (string) $value : $value; } - - /** - * Implements TypedDataInterface::validate(). - */ - public function validate() { - // TODO: Implement validate() method. - } } diff --git a/core/lib/Drupal/Core/TypedData/TypedDataInterface.php b/core/lib/Drupal/Core/TypedData/TypedDataInterface.php index 1a8ffe7..37ad97f 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataInterface.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataInterface.php @@ -57,7 +57,21 @@ public function setValue($value); public function getString(); /** + * Gets a list of validation constraints. + * + * @return array + * Array of constraints, each being an instance of + * \Symfony\Component\Validator\Constraint. + * + */ + public function getConstraints(); + + /** * Validates the currently set data value. + * + * @return \Symfony\Component\Validator\ConstraintViolationListInterface + * A list of constraint violations. If the list is empty, validation + * succeeded. */ public function validate(); } diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php index a1522d8..39f3d1e 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -10,12 +10,32 @@ use Drupal\Component\Plugin\PluginManagerBase; use Drupal\Core\Plugin\Discovery\CacheDecorator; use Drupal\Core\Plugin\Discovery\HookDiscovery; +use Drupal\Core\TypedData\Validation\MetadataFactory; +use Drupal\Core\Validation\ConstraintManager; +use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\ValidatorInterface; /** * Manages data type plugins. */ class TypedDataManager extends PluginManagerBase { + /** + * The validator used for validating typed data. + * + * @var \Symfony\Component\Validator\ValidatorInterface + */ + protected $validator; + + /** + * The validation constraint manager to use for instantiating constraints. + * + * @var \Drupal\Core\Validation\ConstraintManager + */ + protected $constraintManager; + + public function __construct() { $this->discovery = new CacheDecorator(new HookDiscovery('data_type_info'), 'typed_data:types'); $this->factory = new TypedDataFactory($this->discovery); @@ -108,4 +128,53 @@ function create(array $definition, $value = NULL, array $context = array()) { } return $wrapper; } + + /** + * Sets the validator for validating typed data. + * + * @param \Symfony\Component\Validator\ValidatorInterface $validator + * The validator object to set. + */ + public function setValidator(ValidatorInterface $validator) { + $this->validator = $validator; + } + + /** + * Gets the validator for validating typed data. + * + * @return \Symfony\Component\Validator\ValidatorInterface + * The validator object. + */ + public function getValidator() { + if (!isset($this->validator)) { + $this->validator = new Validator(new MetadataFactory(), new ConstraintValidatorFactory()); + } + return $this->validator; + } + + /** + * Sets the validation constraint manager. + * + * The validation constraint manager is used to instantiate validation + * constraint plugins. + * + * @param \Drupal\Core\Validation\ConstraintManager + * The constraint manager to set. + */ + public function setValidationConstraintManager(ConstraintManager $constraintManager) { + $this->constraintManager = $constraintManager; + } + + /** + * Gets the validation constraint manager. + * + * @return \Drupal\Core\Validation\ConstraintManager + * The constraint manager. + */ + public function getValidationConstraintManager() { + if (!isset($this->constraintManager)) { + $this->constraintManager = drupal_container()->get('validation.constraint'); + } + return $this->constraintManager; + } } diff --git a/core/lib/Drupal/Core/TypedData/Validation/Metadata.php b/core/lib/Drupal/Core/TypedData/Validation/Metadata.php new file mode 100644 index 0000000..e520253 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/Metadata.php @@ -0,0 +1,93 @@ +typedData = $typed_data; + $this->name = $name; + $this->factory = $factory; + } + + /** + * Implements MetadataInterface::accept(). + */ + public function accept(ValidationVisitorInterface $visitor, $typed_data, $group, $propertyPath) { + + // @todo: Do we have to care about groups? Symfony class metadata has + // $propagatedGroup. + + $visitor->visit($this, $typed_data->getValue(), $group, $propertyPath); + } + + /** + * Implements MetadataInterface::findConstraints(). + */ + public function findConstraints($group) { + return $this->typedData->getConstraints(); + } + + /** + * Returns the name of the property. + * + * @return string The property name. + */ + public function getPropertyName() { + return $this->name; + } + + /** + * Extracts the value of the property from the given container. + * + * @param mixed $container The container to extract the property value from. + * + * @return mixed The value of the property. + */ + public function getPropertyValue($container) { + return $this->typedData->getValue(); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php b/core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php new file mode 100644 index 0000000..989119d --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php @@ -0,0 +1,43 @@ +visit($this, $typed_data, $group, $propertyPath); + $pathPrefix = empty($propertyPath) ? '' : $propertyPath . '.'; + + foreach ($this->typedData as $name => $data) { + $metadata = $this->factory->getMetadataFor($typed_data, $name); + $metadata->accept($visitor, $data, $group, $pathPrefix . $name); + } + } + + /** + * Implements PropertyMetadataContainerInterface::getPropertyMetadata(). + */ + public function getPropertyMetadata($property_name) { + if ($this->typedData instanceof ListInterface) { + return array(new PropertyMetadata($this->typedData[$property_name], $property_name)); + } + elseif ($this->typedData instanceof ComplexDataInterface) { + return array(new PropertyMetadata($this->typedData->get($property_name), $property_name)); + } + else { + throw new \LogicException("There are no known properties."); + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Validation/ConstraintManager.php b/core/lib/Drupal/Core/Validation/ConstraintManager.php new file mode 100644 index 0000000..c9da430 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php @@ -0,0 +1,55 @@ +discovery = new AlterDecorator(new DerivativeDiscoveryDecorator($discovery), 'validation_constraint'); + $this->factory = new DefaultFactory($this->discovery); + } + + /** + * Returns a list of Constraints that apply to a certain type. + * + * @param string $type + * The type to filter on. + * @param boolean $include_general + * Whether or not to include the ones with an empty type. + */ + public function getList($type = NULL, $include_general = TRUE) { + $constraints = array(); + $definitions = $this->getDefinitions(); + foreach ($definitions as $plugin_id => $definition) { + if ($type === NULL || $definition['type'] == $type) { + $constraints[$plugin_id] = $definition; + } + elseif ($type === '' && $include_general) { + $constraints[$plugin_id] = $definition; + } + } + return $constraints; + } +} diff --git a/core/lib/Drupal/Core/Validation/SymfonyDiscoveryDecorator.php b/core/lib/Drupal/Core/Validation/SymfonyDiscoveryDecorator.php new file mode 100644 index 0000000..fc2224f --- /dev/null +++ b/core/lib/Drupal/Core/Validation/SymfonyDiscoveryDecorator.php @@ -0,0 +1,95 @@ +decorated = $decorated; + } + + /** + * Implements \Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinition(). + */ + public function getDefinition($plugin_id) { + $definitions = $this->getDefinitions(); + if (isset($definitions[$plugin_id])) { + return $definitions[$plugin_id]; + } + } + + /** + * Implements \Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinitions(). + */ + public function getDefinitions() { + $definitions = $this->decorated->getDefinitions(); + // @todo: Care about types. + + $definitions['Min'] = array( + 'label' => t('Minimum value'), + 'class' => '\Symfony\Component\Validator\Constraints\Min' + ); + $definitions['MinLength'] = array( + 'label' => t('Minimum length'), + 'class' => '\Symfony\Component\Validator\Constraints\MinLength' + ); + $definitions['Max'] = array( + 'label' => t('Maximum value'), + 'class' => '\Symfony\Component\Validator\Constraints\Max' + ); + $definitions['MaxLength'] = array( + 'label' => t('Maximum length'), + 'class' => '\Symfony\Component\Validator\Constraints\MaxLength' + ); + $definitions['Null'] = array( + 'label' => t('Null'), + 'class' => '\Symfony\Component\Validator\Constraints\Null' + ); + $definitions['NotNull'] = array( + 'label' => t('Not null'), + 'class' => '\Symfony\Component\Validator\Constraints\NotNull' + ); + $definitions['Blank'] = array( + 'label' => t('Blank'), + 'class' => '\Symfony\Component\Validator\Constraints\Blank' + ); + $definitions['NotBlank'] = array( + 'label' => t('Not blank'), + 'class' => '\Symfony\Component\Validator\Constraints\NotBlank' + ); + + return $definitions; + } + + /** + * Passes through all unknown calls onto the decorated object + */ + public function __call($method, $args) { + return call_user_func_array(array($this->decorated, $method), $args); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php index 00e4c95..c54f3f1 100644 --- a/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/TypedData/TypedDataTest.php @@ -16,6 +16,13 @@ */ class TypedDataTest extends WebTestBase { + /** + * The typed data manager to use. + * + * @var \Drupal\Core\TypedData\TypedDataManager + */ + protected $typedData; + public static function getInfo() { return array( 'name' => 'Test typed data objects', @@ -24,6 +31,11 @@ public static function getInfo() { ); } + public function setUp() { + parent::setup(); + $this->typedData = typed_data(); + } + /** * Tests the basics around constructing and working with data wrappers. */ @@ -127,4 +139,65 @@ public function testGetAndSet() { $wrapper->setValue(NULL); $this->assertNull($wrapper->getValue(), 'Binary wrapper is null-able.'); } + + /** + * Tests typed data validation. + */ + public function testTypedDataValidation() { + $definition = array( + 'type' => 'integer', + 'constraints' => array( + 'Min' => array('limit' => 5), + ), + ); + $violations = $this->typedData->create($definition, 10)->validate(); + $this->assertEqual($violations->count(), 0); + + $integer = $this->typedData->create($definition, 1); + $violations = $integer->validate(); + $this->assertEqual($violations->count(), 1); + // @todo: Translation. + $this->assertEqual($violations[0]->getMessage(), 'This value should be 5 or more.'); + $this->assertEqual($violations[0]->getPropertyPath(), ''); + $this->assertEqual($violations[0]->getPropertyPath(), ''); + $this->assertIdentical($violations[0]->getRoot(), $integer, 'Root object returned.'); + + // Test having multiple violations. + $definition = array( + 'type' => 'integer', + 'constraints' => array( + 'Min' => array('limit' => 5), + 'Null' => array(), + ), + ); + $violations = $this->typedData->create($definition, 10)->validate(); + $this->assertEqual($violations->count(), 1); + $violations = $this->typedData->create($definition, 1)->validate(); + $this->assertEqual($violations->count(), 2); + + // Test validating property containers. + $definition = array( + 'type' => 'integer_field', + 'constraints' => array( + 'NotNull' => array(), + ), + ); + // Tests the Min constraint on the integer item. + // @see IntegerItem::getPropertyDefinitions() + // @todo: Replace it with a proper test. + $field_item = $this->typedData->create($definition, array('value' => 10)); + $violations = $field_item->validate(); + $this->assertEqual($violations->count(), 0); + + $field_item = $this->typedData->create($definition, array('value' => 1)); + $violations = $field_item->validate(); + $this->assertEqual($violations->count(), 1); + + // Test that the field item may not be empty. + $field_item = $this->typedData->create($definition); + $violations = $field_item->validate(); + // @todo: This does not work as an empty typed data object is passed to + // the constraint. Fix that. + $this->assertEqual($violations->count(), 1); + } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Validation/ValidationConstraintTest.php b/core/modules/system/lib/Drupal/system/Tests/Validation/ValidationConstraintTest.php new file mode 100644 index 0000000..6b38926 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Validation/ValidationConstraintTest.php @@ -0,0 +1,35 @@ + 'ValidationConstraintTest', + 'description' => "Test constraint.", + 'group' => 'Validation', + ); + } + + /** + * Tests NotNullConstraint. + */ + public function testNotNullConstraint() { + $constraint = new Constraint\NotNullConstraint(array(), array()); + $this->assertTrue($constraint->validate(1)); + $this->assertTrue($constraint->validate(0)); + $this->assertFalse($constraint->validate(NULL)); + } +}