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/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index 35ef89d..6c1baeb 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -164,8 +164,8 @@ public function getPropertyDefinition($name) { */ public function getPropertyDefinitions() { return entity_get_controller($this->entityType)->getFieldDefinitions(array( - 'entity type' => $this->entityType, - 'bundle' => $this->bundle(), + 'EntityType' => $this->entityType, + 'Bundle' => $this->bundle(), )); } diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php index f5a596d..c1977ed 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php @@ -125,8 +125,8 @@ public function save(EntityInterface $entity); * 'bundle' key. For example: * @code * array( - * 'entity type' => 'node', - * 'bundle' => 'article', + * 'EntityType' => 'node', + * 'Bundle' => 'article', * ) * @endcode * 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..99e9c6b 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php @@ -39,11 +39,14 @@ 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', 'constraints' => array( - 'entity type' => $entity_type, + 'EntityType' => $entity_type, ), 'label' => t('Entity'), 'description' => t('The referenced 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..c712868 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php @@ -29,8 +29,8 @@ * an 'entity type' constraint is specified. * * Supported constraints (below the definition's 'constraints' key) are: - * - entity type: The entity type. - * - bundle: The bundle or an array of possible bundles. + * - EntityType: The entity type. + * - Bundle: The bundle or an array of possible bundles. * * Supported settings (below the definition's 'settings' key) are: * - id source: If used as computed property, the ID property used to load @@ -71,7 +71,7 @@ class EntityWrapper extends TypedData implements IteratorAggregate, ComplexDataI */ public function __construct(array $definition) { $this->definition = $definition + array('constraints' => array()); - $this->entityType = isset($this->definition['constraints']['entity type']) ? $this->definition['constraints']['entity type'] : NULL; + $this->entityType = isset($this->definition['constraints']['EntityType']) ? $this->definition['constraints']['EntityType'] : NULL; } /** @@ -103,7 +103,7 @@ public function setValue($value) { $this->entityType = $value->entityType(); $value = $value->id(); } - elseif (isset($value) && !(is_scalar($value) && !empty($this->definition['constraints']['entity type']))) { + elseif (isset($value) && !(is_scalar($value) && !empty($this->definition['constraints']['EntityType']))) { throw new InvalidArgumentException('Value is no valid entity.'); } @@ -125,18 +125,12 @@ public function getString() { } /** - * Implements TypedDataInterface::validate(). - */ - public function validate($value = NULL) { - // TODO: Implement validate() method. - } - - /** * Implements IteratorAggregate::getIterator(). */ public function getIterator() { $entity = $this->getValue(); - return $entity ? $entity->getIterator() : new ArrayIterator(array()); + // @todo: Remove check for EntityNG once all entity types are converted. + return $entity && $entity instanceof \Drupal\Core\Entity\EntityNG ? $entity->getIterator() : new ArrayIterator(array()); } /** 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/Plugin/Validation/Constraint/EntityType.php b/core/lib/Drupal/Core/Plugin/Validation/Constraint/EntityType.php new file mode 100644 index 0000000..3653526 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Validation/Constraint/EntityType.php @@ -0,0 +1,53 @@ +getValue(); + + if (!empty($entity) && $entity->entityType() != $constraint->type) { + $this->context->addViolation($constraint->message, array('{ type }', $constraint->type)); + } + } +} diff --git a/core/lib/Drupal/Core/Plugin/Validation/Constraint/NotNull.php b/core/lib/Drupal/Core/Plugin/Validation/Constraint/NotNull.php new file mode 100644 index 0000000..5ab7acd --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Validation/Constraint/NotNull.php @@ -0,0 +1,22 @@ +isEmpty(); + } + else { + $fail = $value->getValue() === NULL; + } + } + + if ($fail) { + $this->context->addViolation($constraint->message); + } + } +} diff --git a/core/lib/Drupal/Core/Plugin/Validation/Constraint/Null.php b/core/lib/Drupal/Core/Plugin/Validation/Constraint/Null.php new file mode 100644 index 0000000..9784752 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Validation/Constraint/Null.php @@ -0,0 +1,22 @@ +isEmpty(); + } + else { + $fail = $value->getValue() !== NULL; + } + } + + if ($fail) { + $this->context->addViolation($constraint->message); + } + } +} diff --git a/core/lib/Drupal/Core/Plugin/Validation/Constraint/PrimitiveType.php b/core/lib/Drupal/Core/Plugin/Validation/Constraint/PrimitiveType.php new file mode 100644 index 0000000..6182d27 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Validation/Constraint/PrimitiveType.php @@ -0,0 +1,22 @@ +type) { + case Primitive::BINARY: + $valid = is_resource($value); + break; + case Primitive::BOOLEAN: + $valid = is_bool($value) || $value === 0 || $value === '0' || $value === 1 || $value == '1'; + break; + case Primitive::DATE: + $valid = $value instanceOf DrupalDateTime && !$value->hasErrors(); + break; + case Primitive::DURATION: + $valid = $value instanceof DateInterval; + break; + case Primitive::FLOAT: + $valid = is_numeric($value); + break; + case Primitive::INTEGER: + // Also consider integer values represented as PHP string as valid. + $valid = ((string) (int) $value === (string) $value); + break; + case Primitive::STRING: + $valid = is_scalar($value); + break; + case Primitive::URI: + // @todo: Support any URI which is valid according to rfc2396. + $valid = is_scalar($value) && valid_url($value, TRUE); + break; + default: + $valid = FALSE; + break; + } + + if (!$valid) { + $this->context->addViolation($constraint->message, array( + '{{ value }}' => is_object($value) ? get_class($value) : (is_array($value) ? 'Array' : (string) $value), + '{{ type }}' => $constraint->type, + )); + } + } +} diff --git a/core/lib/Drupal/Core/TypedData/Type/Binary.php b/core/lib/Drupal/Core/TypedData/Type/Binary.php index 7ec5e6b..ef9b295 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Binary.php +++ b/core/lib/Drupal/Core/TypedData/Type/Binary.php @@ -38,7 +38,7 @@ class Binary extends TypedData implements TypedDataInterface { */ public function getValue() { if (!isset($this->handle) && isset($this->uri)) { - $this->handle = fopen($this->uri, 'rb'); + $this->handle = is_readable($this->uri) ? fopen($this->uri, 'rb') : FALSE; } return $this->handle; } @@ -51,16 +51,14 @@ public function setValue($value) { $this->handle = NULL; $this->uri = NULL; } - elseif (is_resource($value)) { - $this->handle = $value; - } elseif (is_string($value)) { // Note: For performance reasons we store the given URI and access the // resource upon request. See Binary::getValue() $this->uri = $value; + $this->handle = NULL; } else { - throw new InvalidArgumentException("Invalid value for binary data given."); + $this->handle = $value; } } @@ -74,11 +72,4 @@ public function getString() { } return $contents; } - - /** - * Implements TypedDataInterface::validate(). - */ - public function validate() { - // TODO: Implement validate() method. - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Boolean.php b/core/lib/Drupal/Core/TypedData/Type/Boolean.php index 5714599..10ebad8 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Boolean.php +++ b/core/lib/Drupal/Core/TypedData/Type/Boolean.php @@ -23,18 +23,4 @@ class Boolean extends TypedData implements TypedDataInterface { * @var boolean */ protected $value; - - /** - * Implements TypedDataInterface::setValue(). - */ - public function setValue($value) { - $this->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..37253ca 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Date.php +++ b/core/lib/Drupal/Core/TypedData/Type/Date.php @@ -40,9 +40,6 @@ public function setValue($value) { } else { $this->value = $value instanceOf DrupalDateTime ? $value : new DrupalDateTime($value); - if ($this->value->hasErrors()) { - throw new InvalidArgumentException("Invalid date format given."); - } } } @@ -52,11 +49,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..537af0f 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Duration.php +++ b/core/lib/Drupal/Core/TypedData/Type/Duration.php @@ -32,21 +32,29 @@ class Duration extends TypedData implements TypedDataInterface { * Implements TypedDataInterface::setValue(). */ public function setValue($value) { - if ($value instanceof DateInterval || !isset($value)) { - $this->value = $value; + try { + if ($value instanceof DateInterval || !isset($value)) { + $this->value = $value; + } + // Treat integer values as time spans in seconds, even if supplied as PHP + // string. + elseif ((string) (int) $value === (string) $value) { + $this->value = new DateInterval('PT' . $value . 'S'); + } + elseif (is_string($value)) { + // @todo: Add support for negative intervals on top of the DateInterval + // constructor. + $this->value = new DateInterval($value); + } + else { + // Unknown value given. + $this->value = $value; + } } - // Treat integer values as time spans in seconds, even if supplied as PHP - // string. - elseif ((string) (int) $value === (string) $value) { - $this->value = new DateInterval('PT' . $value . 'S'); - } - elseif (is_string($value)) { - // @todo: Add support for negative intervals on top of the DateInterval - // constructor. - $this->value = new DateInterval($value); - } - else { - throw new InvalidArgumentException("Invalid duration format given."); + catch (\Exception $e) { + // An invalid value has been given. Setting any invalid value will let + // validation fail. + $this->value = $e; } } @@ -58,11 +66,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..8c3fedc 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Float.php +++ b/core/lib/Drupal/Core/TypedData/Type/Float.php @@ -23,18 +23,4 @@ class Float extends TypedData implements TypedDataInterface { * @var float */ protected $value; - - /** - * Implements TypedDataInterface::setValue(). - */ - 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..5c814f5 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Integer.php +++ b/core/lib/Drupal/Core/TypedData/Type/Integer.php @@ -23,18 +23,4 @@ class Integer extends TypedData implements TypedDataInterface { * @var integer */ protected $value; - - /** - * Implements TypedDataInterface::setValue(). - */ - 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..3f6670d 100644 --- a/core/lib/Drupal/Core/TypedData/Type/String.php +++ b/core/lib/Drupal/Core/TypedData/Type/String.php @@ -23,18 +23,4 @@ class String extends TypedData implements TypedDataInterface { * @var string */ protected $value; - - /** - * Implements TypedDataInterface::setValue(). - */ - 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..57cb6ba 100644 --- a/core/lib/Drupal/Core/TypedData/Type/TypedData.php +++ b/core/lib/Drupal/Core/TypedData/Type/TypedData.php @@ -70,4 +70,41 @@ public function setValue($value) { public function getString() { return (string) $this->getValue(); } + + /** + * Implements TypedDataInterface::getConstraints(). + */ + public function getConstraints() { + $constraints = array(); + // @todo: Add the typed data manager as proper dependency. + // @todo: Figure out how to handle nested constraint structures as + // collections. + $type_definition = typed_data()->getDefinition($this->definition['type']); + // Auto-generate a constraint for the primitive type if we have a mapping. + if (isset($type_definition['primitive type'])) { + $constraints[] = typed_data()->getValidationConstraintManager()-> + createInstance('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[] = typed_data()->createValidationConstraint($name, $options); + } + } + // Add any constraints specified as part of the data definition. + if (isset($this->definition['constraints'])) { + foreach ($this->definition['constraints'] as $name => $options) { + $constraints[] = typed_data()->createValidationConstraint($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..64b665d 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Uri.php +++ b/core/lib/Drupal/Core/TypedData/Type/Uri.php @@ -22,18 +22,4 @@ class Uri extends TypedData implements TypedDataInterface { * @var string */ protected $value; - - /** - * Implements TypedDataInterface::setValue(). - */ - 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..a1c1128 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 Drupal\Core\Validation\Validator; +use Symfony\Component\Validator\ConstraintValidatorFactory; +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); @@ -60,9 +80,12 @@ public function createInstance($plugin_id, array $configuration) { * - list settings: An array of settings as required by the used * 'list class'. See the documentation of the list class for support or * required settings. - * - constraints: An array of type specific value constraints, e.g. for data - * of type 'entity' the 'entity type' and 'bundle' may be specified. See - * the documentation of the data type 'class' for supported constraints. + * - constraints: An array of validation constraints. Array keys are + * validation constraint plugin IDs, the values the array of constraint + * options. The specified constraints must be compatible with the data + * type, e.g. for data of type 'entity' the 'EntityType' and 'Bundle' + * constraints may be specified. + * See \Drupal\Core\Validation\ConstraintManager. * - required: A boolean specifying whether a non-NULL value is mandatory. * Further keys may be supported in certain usages, e.g. for further keys * supported for entity field definitions see @@ -108,4 +131,75 @@ 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; + } + + /** + * 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 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); + } } 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($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..54abd65 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php @@ -0,0 +1,59 @@ +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/ConstraintViolation.php b/core/lib/Drupal/Core/Validation/ConstraintViolation.php new file mode 100644 index 0000000..ed59545 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/ConstraintViolation.php @@ -0,0 +1,31 @@ +getMessageParameters(); + + foreach ($parameters as $i => $parameter) { + if (is_array($parameter)) { + $parameters[$i] = 'Array'; + } + } + // t() will escape Symfony message replacements like '{ limit }' as + // placeholder. See format_string(). + return t($this->getMessageTemplate(), $parameters); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Validation/ExecutionContext.php b/core/lib/Drupal/Core/Validation/ExecutionContext.php new file mode 100644 index 0000000..360cbda --- /dev/null +++ b/core/lib/Drupal/Core/Validation/ExecutionContext.php @@ -0,0 +1,31 @@ +globalContext->getViolations()->add(new ConstraintViolation( + $message, + $params, + $this->globalContext->getRoot(), + $this->getPropertyPath(), + // check using func_num_args() to allow passing NULL values + func_num_args() >= 3 ? $invalidValue : $this->getValue(), + $pluralization, + $code + )); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Validation/SymfonyDiscoveryDecorator.php b/core/lib/Drupal/Core/Validation/SymfonyDiscoveryDecorator.php new file mode 100644 index 0000000..8c8a4bc --- /dev/null +++ b/core/lib/Drupal/Core/Validation/SymfonyDiscoveryDecorator.php @@ -0,0 +1,87 @@ +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['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/lib/Drupal/Core/Validation/ValidationVisitor.php b/core/lib/Drupal/Core/Validation/ValidationVisitor.php new file mode 100644 index 0000000..ffef0cf --- /dev/null +++ b/core/lib/Drupal/Core/Validation/ValidationVisitor.php @@ -0,0 +1,31 @@ +validateValue($value, $metadata->findConstraints($group)); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Validation/Validator.php b/core/lib/Drupal/Core/Validation/Validator.php new file mode 100644 index 0000000..84e9db3 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/Validator.php @@ -0,0 +1,37 @@ +metadataFactory, $this->validatorFactory, $this->objectInitializers); + } +} \ No newline at end of file diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php index a152bfa..a6caffe 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php @@ -280,7 +280,7 @@ public function testIntrospection() { $definition = array( 'type' => 'entity', 'constraints' => array( - 'entity type' => 'entity_test', + 'EntityType' => 'entity_test', ), 'label' => t('Test entity'), ); @@ -342,23 +342,23 @@ public function testIterator() { } /** - * Tests working with entity properties based upon data structure and data - * list interfaces. + * Tests working with the entity based upon the TypedData API. */ - public function testDataStructureInterfaces() { + public function testTypedDataAPI() { $entity = $this->createTestEntity(); $entity->save(); $entity_definition = array( 'type' => 'entity', 'constraints' => array( - 'entity type' => 'entity_test', + 'EntityType' => 'entity_test', ), 'label' => t('Test entity'), ); $wrapped_entity = typed_data()->create($entity_definition, $entity); - // For the test we navigate through the tree of contained properties and get - // all contained strings, limited by a certain depth. + // Test using the whole tree of typed data by navigating through the tree of + // contained properties and getting all contained strings, limited by a + // certain depth. $strings = array(); $this->getContainedStrings($wrapped_entity, 0, $strings); @@ -373,6 +373,16 @@ public function testDataStructureInterfaces() { NULL, ); $this->assertEqual($strings, $target_strings, 'All contained strings found.'); + + // Test validation the typed data object. + $violations = $wrapped_entity->validate(); + $this->assertEqual($violations->count(), 0); + + // Test validating an entity of the wrong type. + $node = $this->drupalCreateNode(); + $wrapped_entity->setValue($node); + $violations = $wrapped_entity->validate(); + $this->assertEqual($violations->count(), 1); } /** 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..ddabb43 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,107 +31,229 @@ public static function getInfo() { ); } + public function setUp() { + parent::setup(); + $this->typedData = typed_data(); + } + /** - * Tests the basics around constructing and working with data wrappers. + * Tests the basics around constructing and working with typed data objects. */ public function testGetAndSet() { // Boolean type. - $wrapper = $this->createTypedData(array('type' => 'boolean'), TRUE); - $this->assertTrue($wrapper->getValue() === TRUE, 'Boolean value was fetched.'); - $wrapper->setValue(FALSE); - $this->assertTrue($wrapper->getValue() === FALSE, 'Boolean value was changed.'); - $this->assertTrue(is_string($wrapper->getString()), 'Boolean value was converted to string'); - $wrapper->setValue(NULL); - $this->assertNull($wrapper->getValue(), 'Boolean wrapper is null-able.'); + $typed_data = $this->createTypedData(array('type' => 'boolean'), TRUE); + $this->assertTrue($typed_data->getValue() === TRUE, 'Boolean value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(FALSE); + $this->assertTrue($typed_data->getValue() === FALSE, 'Boolean value was changed.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $this->assertTrue(is_string($typed_data->getString()), 'Boolean value was converted to string'); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Boolean wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); // String type. $value = $this->randomString(); - $wrapper = $this->createTypedData(array('type' => 'string'), $value); - $this->assertTrue($wrapper->getValue() === $value, 'String value was fetched.'); + $typed_data = $this->createTypedData(array('type' => 'string'), $value); + $this->assertTrue($typed_data->getValue() === $value, 'String value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); $new_value = $this->randomString(); - $wrapper->setValue($new_value); - $this->assertTrue($wrapper->getValue() === $new_value, 'String value was changed.'); + $typed_data->setValue($new_value); + $this->assertTrue($typed_data->getValue() === $new_value, 'String value was changed.'); + $this->assertEqual($typed_data->validate()->count(), 0); // Funky test. - $this->assertTrue(is_string($wrapper->getString()), 'String value was converted to string'); - $wrapper->setValue(NULL); - $this->assertNull($wrapper->getValue(), 'String wrapper is null-able.'); + $this->assertTrue(is_string($typed_data->getString()), 'String value was converted to string'); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'String wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(array('no string')); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); // Integer type. $value = rand(); - $wrapper = $this->createTypedData(array('type' => 'integer'), $value); - $this->assertTrue($wrapper->getValue() === $value, 'Integer value was fetched.'); + $typed_data = $this->createTypedData(array('type' => 'integer'), $value); + $this->assertTrue($typed_data->getValue() === $value, 'Integer value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); $new_value = rand(); - $wrapper->setValue($new_value); - $this->assertTrue($wrapper->getValue() === $new_value, 'Integer value was changed.'); - $this->assertTrue(is_string($wrapper->getString()), 'Integer value was converted to string'); - $wrapper->setValue(NULL); - $this->assertNull($wrapper->getValue(), 'Integer wrapper is null-able.'); + $typed_data->setValue($new_value); + $this->assertTrue($typed_data->getValue() === $new_value, 'Integer value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'Integer value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Integer wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); // Float type. $value = 123.45; - $wrapper = $this->createTypedData(array('type' => 'float'), $value); - $this->assertTrue($wrapper->getValue() === $value, 'Float value was fetched.'); + $typed_data = $this->createTypedData(array('type' => 'float'), $value); + $this->assertTrue($typed_data->getValue() === $value, 'Float value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); $new_value = 678.90; - $wrapper->setValue($new_value); - $this->assertTrue($wrapper->getValue() === $new_value, 'Float value was changed.'); - $this->assertTrue(is_string($wrapper->getString()), 'Float value was converted to string'); - $wrapper->setValue(NULL); - $this->assertNull($wrapper->getValue(), 'Float wrapper is null-able.'); + $typed_data->setValue($new_value); + $this->assertTrue($typed_data->getValue() === $new_value, 'Float value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'Float value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Float wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); // Date type. $value = new DrupalDateTime(REQUEST_TIME); - $wrapper = $this->createTypedData(array('type' => 'date'), $value); - $this->assertTrue($wrapper->getValue() === $value, 'Date value was fetched.'); + $typed_data = $this->createTypedData(array('type' => 'date'), $value); + $this->assertTrue($typed_data->getValue() === $value, 'Date value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); $new_value = REQUEST_TIME + 1; - $wrapper->setValue($new_value); - $this->assertTrue($wrapper->getValue()->getTimestamp() === $new_value, 'Date value was changed and set by timestamp.'); - $wrapper->setValue('2000-01-01'); - $this->assertTrue($wrapper->getValue()->format('Y-m-d') == '2000-01-01', 'Date value was changed and set by date string.'); - $this->assertTrue(is_string($wrapper->getString()), 'Date value was converted to string'); - $wrapper->setValue(NULL); - $this->assertNull($wrapper->getValue(), 'Date wrapper is null-able.'); + $typed_data->setValue($new_value); + $this->assertTrue($typed_data->getValue()->getTimestamp() === $new_value, 'Date value was changed and set by timestamp.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('2000-01-01'); + $this->assertTrue($typed_data->getValue()->format('Y-m-d') == '2000-01-01', 'Date value was changed and set by date string.'); + $this->assertTrue(is_string($typed_data->getString()), 'Date value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Date wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); // Duration type. $value = new DateInterval('PT20S'); - $wrapper = $this->createTypedData(array('type' => 'duration'), $value); - $this->assertTrue($wrapper->getValue() === $value, 'Duration value was fetched.'); - $wrapper->setValue(10); - $this->assertTrue($wrapper->getValue()->s == 10, 'Duration value was changed and set by time span in seconds.'); - $wrapper->setValue('P40D'); - $this->assertTrue($wrapper->getValue()->d == 40, 'Duration value was changed and set by duration string.'); - $this->assertTrue(is_string($wrapper->getString()), 'Duration value was converted to string'); + $typed_data = $this->createTypedData(array('type' => 'duration'), $value); + $this->assertTrue($typed_data->getValue() === $value, 'Duration value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(10); + $this->assertTrue($typed_data->getValue()->s == 10, 'Duration value was changed and set by time span in seconds.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('P40D'); + $this->assertTrue($typed_data->getValue()->d == 40, 'Duration value was changed and set by duration string.'); + $this->assertTrue(is_string($typed_data->getString()), 'Duration value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); // Test getting the string and passing it back as value. - $duration = $wrapper->getString(); - $wrapper->setValue($duration); - $this->assertEqual($wrapper->getString(), $duration, 'Duration formatted as string can be used to set the duration value.'); - $wrapper->setValue(NULL); - $this->assertNull($wrapper->getValue(), 'Duration wrapper is null-able.'); - - // Generate some files that will be used to test the URI and the binary - // data types. - $files = $this->drupalGetTestFiles('image'); + $duration = $typed_data->getString(); + $typed_data->setValue($duration); + $this->assertEqual($typed_data->getString(), $duration, 'Duration formatted as string can be used to set the duration value.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Duration wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); // URI type. - $wrapper = $this->createTypedData(array('type' => 'uri'), $files[0]->uri); - $this->assertTrue($wrapper->getValue() === $files[0]->uri, 'URI value was fetched.'); - $wrapper->setValue($files[1]->uri); - $this->assertTrue($wrapper->getValue() === $files[1]->uri, 'URI value was changed.'); - $this->assertTrue(is_string($wrapper->getString()), 'URI value was converted to string'); - $wrapper->setValue(NULL); - $this->assertNull($wrapper->getValue(), 'URI wrapper is null-able.'); + $uri = 'http://example.com/foo/'; + $typed_data = $this->createTypedData(array('type' => 'uri'), $uri); + $this->assertTrue($typed_data->getValue() === $uri, 'URI value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue($uri . 'bar.txt'); + $this->assertTrue($typed_data->getValue() === $uri . 'bar.txt', 'URI value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'URI value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'URI wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + + // Generate some files that will be used to test the binary data type. + $files = $this->drupalGetTestFiles('image'); // Binary type. - $wrapper = $this->createTypedData(array('type' => 'binary'), $files[0]->uri); - $this->assertTrue(is_resource($wrapper->getValue()), 'Binary value was fetched.'); + $typed_data = $this->createTypedData(array('type' => 'binary'), $files[0]->uri); + $this->assertTrue(is_resource($typed_data->getValue()), 'Binary value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); // Try setting by URI. - $wrapper->setValue($files[1]->uri); - $this->assertEqual(is_resource($wrapper->getValue()), fopen($files[1]->uri, 'r'), 'Binary value was changed.'); - $this->assertTrue(is_string($wrapper->getString()), 'Binary value was converted to string'); + $typed_data->setValue($files[1]->uri); + $this->assertEqual(is_resource($typed_data->getValue()), fopen($files[1]->uri, 'r'), 'Binary value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'Binary value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); // Try setting by resource. - $wrapper->setValue(fopen($files[2]->uri, 'r')); - $this->assertEqual(is_resource($wrapper->getValue()), fopen($files[2]->uri, 'r'), 'Binary value was changed.'); - $this->assertTrue(is_string($wrapper->getString()), 'Binary value was converted to string'); - $wrapper->setValue(NULL); - $this->assertNull($wrapper->getValue(), 'Binary wrapper is null-able.'); + $typed_data->setValue(fopen($files[2]->uri, 'r')); + $this->assertEqual(is_resource($typed_data->getValue()), fopen($files[2]->uri, 'r'), 'Binary value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'Binary value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Binary wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + } + + /** + * 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); + + $message = t('This value should be {{ limit }} or more.', array('{{ limit }}' => 5)); + $this->assertEqual($violations[0]->getMessage(), $message, 'Translated violation message retrieved.'); + $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 and make sure the NotNull and Null + // constraints work with typed data containers. + $definition = array( + 'type' => 'integer_field', + 'constraints' => array( + 'NotNull' => array(), + ), + ); + $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' => 'no integer')); + $violations = $field_item->validate(); + $this->assertEqual($violations->count(), 1); + $this->assertEqual($violations[0]->getPropertyPath(), 'value'); + + // Test that the field item may not be empty. + $field_item = $this->typedData->create($definition); + $violations = $field_item->validate(); + $this->assertEqual($violations->count(), 1); + + // Test the Null constraint with typed data containers. + $definition = array( + 'type' => 'integer_field', + 'constraints' => array( + 'Null' => array(), + ), + ); + $field_item = $this->typedData->create($definition, array('value' => 10)); + $violations = $field_item->validate(); + $this->assertEqual($violations->count(), 1); + $field_item = $this->typedData->create($definition); + $violations = $field_item->validate(); + $this->assertEqual($violations->count(), 0); } }