diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 691e6e7..69003fe 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -113,6 +113,8 @@ public function build(ContainerBuilder $container) { ->setFactoryMethod('getConnection') ->addArgument('slave'); $container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager'); + $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 c486aa8..dafffb9 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..098ebd1 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( + 'Range' => array('min' => 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..57d69d4 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()); } /** @@ -228,6 +222,6 @@ public function setPropertyValues($values) { * Implements ComplexDataInterface::isEmpty(). */ public function isEmpty() { - return (bool) $this->getValue(); + return !$this->getValue(); } } diff --git a/core/lib/Drupal/Core/Entity/Field/Type/Field.php b/core/lib/Drupal/Core/Entity/Field/Type/Field.php index 0e7a982..0a1cc3f 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/Field.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/Field.php @@ -130,10 +130,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/BundleConstraint.php b/core/lib/Drupal/Core/Plugin/Validation/Constraint/BundleConstraint.php new file mode 100644 index 0000000..a9618ae --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Validation/Constraint/BundleConstraint.php @@ -0,0 +1,68 @@ +bundle)) { + $this->bundle = array($this->bundle); + } + return $this->bundle; + } + + /** + * Overrides Constraint::getDefaultOption(). + */ + public function getDefaultOption() { + return 'bundle'; + } + + /** + * Overrides Constraint::getRequiredOptions(). + */ + public function getRequiredOptions() { + return array('bundle'); + } +} diff --git a/core/lib/Drupal/Core/Plugin/Validation/Constraint/BundleConstraintValidator.php b/core/lib/Drupal/Core/Plugin/Validation/Constraint/BundleConstraintValidator.php new file mode 100644 index 0000000..d649ccf --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Validation/Constraint/BundleConstraintValidator.php @@ -0,0 +1,28 @@ +getValue() : FALSE; + + if (!empty($entity) && !in_array($entity->bundle(), $constraint->getBundleOption())) { + $this->context->addViolation($constraint->message, array('%bundle', implode(', ', $constraint->getBundleOption()))); + } + } +} 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..3914190 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Validation/Constraint/EntityTypeConstraint.php @@ -0,0 +1,55 @@ +getValue() : FALSE; + + 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/PrimitiveTypeConstraint.php b/core/lib/Drupal/Core/Plugin/Validation/Constraint/PrimitiveTypeConstraint.php new file mode 100644 index 0000000..8207365 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Validation/Constraint/PrimitiveTypeConstraint.php @@ -0,0 +1,27 @@ +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..346c3c0 100644 --- a/core/lib/Drupal/Core/TypedData/Type/TypedData.php +++ b/core/lib/Drupal/Core/TypedData/Type/TypedData.php @@ -70,4 +70,20 @@ public function setValue($value) { public function getString() { return (string) $this->getValue(); } + + /** + * Implements TypedDataInterface::getConstraints(). + */ + public function getConstraints() { + // @todo: Add the typed data manager as proper dependency. + return typed_data()->getConstraints($this->definition); + } + + /** + * 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..4f83c76 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataInterface.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataInterface.php @@ -57,7 +57,20 @@ 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..a5a4c53 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -10,12 +10,31 @@ 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\ValidatorInterface; +use Symfony\Component\Validator\Validation; /** * 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 +79,8 @@ 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. See + * \Drupal\Core\TypedData\TypedDataManager::getConstraints() for details. * - 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 +126,144 @@ 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 = Validation::createValidatorBuilder() + ->setMetadataFactory(new MetadataFactory()) + ->getValidator(); + } + 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 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 + * of the type's plugin definition, or constraints defined below the data + * definition's constraint' key are taken into account. + * + * Constraints are defined via an array, having constraint plugin IDs as key + * and constraint options as values, e.g. + * @code + * $constraints = array( + * 'Range' => array('min' => 5, 'max' => 10), + * 'NotBlank' => array(), + * ); + * @endcode + * Options have to be specified using another array if the constraint has more + * than one or zero options. If it has exactly one option, the value should be + * specified without nesting it into another array: + * @code + * $constraints = array( + * 'EntityType' => 'node', + * 'Bundle' => 'article', + * ); + * @endcode + * + * Note that 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 + * + * @param array $definition + * A data definition array. + * + * @return array + * Array of constraints, each being an instance of + * \Symfony\Component\Validator\Constraint. + */ + public function getConstraints($definition) { + $constraints = array(); + // @todo: Figure out how to handle nested constraint structures as + // collections. + $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'])); + } + // 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); + } + } + // 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); + } + } + // Add the NotNull constraint for required data. + if (!empty($definition['required']) && empty($definition['constraints']['NotNull'])) { + $constraints[] = $this->createValidationConstraint('NotNull', array()); + } + return $constraints; + } } 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..5cfc5df --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/Metadata.php @@ -0,0 +1,94 @@ +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..2858daf --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php @@ -0,0 +1,44 @@ +isEmpty()) { + $typed_data = NULL; + } + $visitor->visit($this, $typed_data, $group, $propertyPath); + $pathPrefix = empty($propertyPath) ? '' : $propertyPath . '.'; + + if ($typed_data) { + foreach ($typed_data 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..904a818 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php @@ -0,0 +1,84 @@ +discovery = new AnnotatedClassDiscovery('Validation', 'Constraint'); + $this->discovery = new SymfonyDiscoveryDecorator($this->discovery); + $this->discovery = new DerivativeDiscoveryDecorator($this->discovery); + $this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition')); + $this->discovery = new AlterDecorator($this->discovery, 'validation_constraint'); + $this->discovery = new CacheDecorator($this->discovery, 'validation_constraints', 'cache'); + + $this->factory = new DefaultFactory($this->discovery); + } + + /** + * Process definition callback for the ProcessDecorator. + */ + public function processDefinition(&$definition, $plugin_id) { + // Make sure 'type' is set and either an array or FALSE. + if (!isset($definition['type'])) { + $definition['type'] = array(); + } + elseif ($definition['type'] !== FALSE && !is_array($definition['type'])) { + $definition['type'] = array($definition['type']); + } + } + + /** + * Returns a list of constraints that support the given type. + * + * @param string $type + * The type to filter on. + * + * @return array + * An array of constraint plugin definitions supporting the given type, + * keyed by constraint name (plugin ID). + */ + public function getDefinitionsByType($type) { + $definitions = array(); + foreach ($this->getDefinitions() as $plugin_id => $definition) { + if ($definition['type'] === FALSE || in_array($type, $definition['type'])) { + $definitions[$plugin_id] = $definition; + } + } + return $definitions; + } +} diff --git a/core/lib/Drupal/Core/Validation/SymfonyDiscoveryDecorator.php b/core/lib/Drupal/Core/Validation/SymfonyDiscoveryDecorator.php new file mode 100644 index 0000000..38013f4 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/SymfonyDiscoveryDecorator.php @@ -0,0 +1,92 @@ +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(); + + $definitions['Range'] = array( + 'label' => t('Range'), + 'class' => '\Symfony\Component\Validator\Constraints\Range', + 'type' => array('integer', 'float'), + ); + $definitions['Length'] = array( + 'label' => t('Length'), + 'class' => '\Symfony\Component\Validator\Constraints\Length', + 'type' => 'string', + ); + $definitions['Null'] = array( + 'label' => t('Null'), + 'class' => '\Symfony\Component\Validator\Constraints\Null', + 'type' => FALSE, + ); + $definitions['NotNull'] = array( + 'label' => t('Not null'), + 'class' => '\Symfony\Component\Validator\Constraints\NotNull', + 'type' => FALSE, + ); + $definitions['Blank'] = array( + 'label' => t('Blank'), + 'class' => '\Symfony\Component\Validator\Constraints\Blank', + 'type' => FALSE, + ); + $definitions['NotBlank'] = array( + 'label' => t('Not blank'), + 'class' => '\Symfony\Component\Validator\Constraints\NotBlank', + 'type' => FALSE, + ); + + 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/Entity/EntityFieldTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php index 3dd810c..de8a342 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php @@ -286,7 +286,7 @@ public function testIntrospection() { $definition = array( 'type' => 'entity', 'constraints' => array( - 'entity type' => 'entity_test', + 'EntityType' => 'entity_test', ), 'label' => t('Test entity'), ); @@ -348,23 +348,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); @@ -379,6 +379,36 @@ 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(array('type' => 'page')); + $wrapped_entity->setValue($node); + $violations = $wrapped_entity->validate(); + $this->assertEqual($violations->count(), 1); + + // Test bundle validation. + $entity_definition = array( + 'type' => 'entity', + 'constraints' => array( + 'EntityType' => 'node', + 'Bundle' => 'article', + ), + 'label' => t('Test entity'), + ); + $wrapped_entity = typed_data()->create($entity_definition, $node); + + $violations = $wrapped_entity->validate(); + $this->assertEqual($violations->count(), 1); + + $node->type = 'article'; + node_save($node); + $wrapped_entity->setValue($node); + $violations = $wrapped_entity->validate(); + $this->assertEqual($violations->count(), 0); } /** 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..7d83307 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,254 @@ 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( + 'Range' => array('min' => 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)); + // Right now there is no support for getting translated messages, so test + // doing it manually for now. + // @todo: Replace this to use the API for getting translations. + $translation = t($violations[0]->getMessageTemplate(), $violations[0]->getMessageParameters()); + $this->assertEqual($translation, $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( + 'Range' => array('min' => 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); + + // Test getting constraint definitions by type. + $definitions = $this->typedData->getValidationConstraintManager()->getDefinitionsByType('entity'); + $this->assertTrue(isset($definitions['EntityType']), 'Constraint plugin found for type entity.'); + $this->assertTrue(isset($definitions['Null']), 'Constraint plugin found for type entity.'); + $this->assertTrue(isset($definitions['NotNull']), 'Constraint plugin found for type entity.'); + + $definitions = $this->typedData->getValidationConstraintManager()->getDefinitionsByType('string'); + $this->assertFalse(isset($definitions['EntityType']), 'Constraint plugin not found for type string.'); + $this->assertTrue(isset($definitions['Null']), 'Constraint plugin found for type string.'); + $this->assertTrue(isset($definitions['NotNull']), 'Constraint plugin found for type string.'); + + // Test automatic 'required' validation. + $definition = array( + 'type' => 'integer', + 'required' => TRUE, + ); + $violations = $this->typedData->create($definition)->validate(); + $this->assertEqual($violations->count(), 1); + $violations = $this->typedData->create($definition, 0)->validate(); + $this->assertEqual($violations->count(), 0); } } diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 07a5638..039e73a 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -178,6 +178,8 @@ function hook_cron() { * primitive types in \Drupal\Core\TypedData\Primitive. If set, it must be * a constant defined by \Drupal\Core\TypedData\Primitive such as * \Drupal\Core\TypedData\Primitive::String. + * - constraints: An array of validation constraints for this type. See + * \Drupal\Core\TypedData\TypedDataManager::getConstraints() for details. * * @see typed_data() * @see Drupal\Core\TypedData\TypedDataManager::create() diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Type/TaxonomyTermReferenceItem.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Type/TaxonomyTermReferenceItem.php index 8eabe7d..5a0ae47 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/Type/TaxonomyTermReferenceItem.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Type/TaxonomyTermReferenceItem.php @@ -35,7 +35,7 @@ public function getPropertyDefinitions() { self::$propertyDefinitions['entity'] = array( 'type' => 'entity', 'constraints' => array( - 'entity type' => 'taxonomy_term', + 'EntityType' => 'taxonomy_term', ), 'label' => t('Term'), 'description' => t('The referenced taxonomy term'), diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php index eac0b7f..264e04d 100644 --- a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php +++ b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php @@ -156,7 +156,7 @@ function testTranslationUI() { $stored_value = $this->getValue($translation, $property, $default_langcode); $value = is_array($value) ? $value[0]['value'] : $value; $message = format_string('@property correctly stored in the default language.', array('@property' => $property)); - $this->assertIdentical($stored_value, $value, $message); + $this->assertEqual($stored_value, $value, $message); } // Add an entity translation.