diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 624c716..078ab6d 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -146,6 +146,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 e16c6fb..f8d4300 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -218,8 +218,8 @@ public function getPropertyDefinition($name) { public function getPropertyDefinitions() { if (!isset($this->fieldDefinitions)) { $this->fieldDefinitions = drupal_container()->get('plugin.manager.entity')->getStorageController($this->entityType)->getFieldDefinitions(array( - 'entity type' => $this->entityType, - 'bundle' => $this->bundle, + 'EntityType' => $this->entityType, + 'Bundle' => $this->bundle, )); } return $this->fieldDefinitions; diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php index 2811f1c..ef69cb6 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/Type/EntityReferenceItem.php b/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php index f1b07e8..6508a40 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), + ), ); static::$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/EntityWrapper.php b/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php index 89179d1..691868d 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/EntityWrapper.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Entity\Field\Type; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityNG; use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\Core\TypedData\ContextAwareInterface; use Drupal\Core\TypedData\ContextAwareTypedData; @@ -29,8 +30,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 @@ -57,7 +58,7 @@ class EntityWrapper extends ContextAwareTypedData implements IteratorAggregate, */ public function __construct(array $definition, $name = NULL, ContextAwareInterface $parent = NULL) { parent::__construct($definition, $name, $parent); - $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; } /** @@ -89,7 +90,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 not a valid entity.'); } // Now update the value in the source or the local id property. @@ -116,7 +117,9 @@ public function getString() { * Implements \IteratorAggregate::getIterator(). */ public function getIterator() { - if ($entity = $this->getValue()) { + // @todo: Remove check for EntityNG once all entity types are converted. + $entity = $this->getValue(); + if ($entity && $entity instanceof EntityNG) { return $entity->getIterator(); } return new ArrayIterator(array()); @@ -193,6 +196,6 @@ public function setPropertyValues($values) { * Implements \Drupal\Core\TypedData\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 34baa61..49418a0 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/Field.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/Field.php @@ -117,6 +117,14 @@ public function getString() { } /** + * Overrides \Drupal\Core\TypedData\TypedData::getConstraints(). + */ + public function getConstraints() { + // Apply the constraints to the list items only. + return array(); + } + + /** * Implements \ArrayAccess::offsetExists(). */ public function offsetExists($offset) { 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 479ddf4..cf25c73 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Binary.php +++ b/core/lib/Drupal/Core/TypedData/Type/Binary.php @@ -40,7 +40,7 @@ public function getValue() { // If the value has been set by (absolute) stream resource URI, access the // resource now. 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; } @@ -55,16 +55,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; } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Boolean.php b/core/lib/Drupal/Core/TypedData/Type/Boolean.php index 62a6791..6c357c9 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Boolean.php +++ b/core/lib/Drupal/Core/TypedData/Type/Boolean.php @@ -23,11 +23,4 @@ class Boolean extends TypedData { * @var boolean */ protected $value; - - /** - * Overrides TypedData::setValue(). - */ - public function setValue($value) { - $this->value = isset($value) ? (bool) $value : $value; - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Date.php b/core/lib/Drupal/Core/TypedData/Type/Date.php index f6c19e0..37a55ff 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Date.php +++ b/core/lib/Drupal/Core/TypedData/Type/Date.php @@ -40,9 +40,13 @@ 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."); - } } } + + /** + * Overrides TypedDataInterface::getString(). + */ + public function getString() { + return (string) $this->getValue(); + } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Duration.php b/core/lib/Drupal/Core/TypedData/Type/Duration.php index 527fbdd..985d38d 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 { * Overrides TypedData::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; } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Float.php b/core/lib/Drupal/Core/TypedData/Type/Float.php index fe7c8b3..482cad1 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Float.php +++ b/core/lib/Drupal/Core/TypedData/Type/Float.php @@ -23,11 +23,4 @@ class Float extends TypedData { * @var float */ protected $value; - - /** - * Overrides TypedData::setValue(). - */ - public function setValue($value) { - $this->value = isset($value) ? (float) $value : $value; - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Integer.php b/core/lib/Drupal/Core/TypedData/Type/Integer.php index 487e1eb..eb6336e 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Integer.php +++ b/core/lib/Drupal/Core/TypedData/Type/Integer.php @@ -23,11 +23,4 @@ class Integer extends TypedData { * @var integer */ protected $value; - - /** - * Overrides TypedData::setValue(). - */ - public function setValue($value) { - $this->value = isset($value) ? (int) $value : $value; - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/String.php b/core/lib/Drupal/Core/TypedData/Type/String.php index f5402ad..a9d096f 100644 --- a/core/lib/Drupal/Core/TypedData/Type/String.php +++ b/core/lib/Drupal/Core/TypedData/Type/String.php @@ -23,11 +23,4 @@ class String extends TypedData { * @var string */ protected $value; - - /** - * Overrides TypedData::setValue(). - */ - public function setValue($value) { - $this->value = isset($value) ? (string) $value : $value; - } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Uri.php b/core/lib/Drupal/Core/TypedData/Type/Uri.php index 3dd893d..791dec9 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Uri.php +++ b/core/lib/Drupal/Core/TypedData/Type/Uri.php @@ -22,11 +22,4 @@ class Uri extends TypedData { * @var string */ protected $value; - - /** - * Overrides TypedData::setValue(). - */ - public function setValue($value) { - $this->value = isset($value) ? (string) $value : $value; - } } diff --git a/core/lib/Drupal/Core/TypedData/TypedData.php b/core/lib/Drupal/Core/TypedData/TypedData.php index 35e168c..d6ad4be 100644 --- a/core/lib/Drupal/Core/TypedData/TypedData.php +++ b/core/lib/Drupal/Core/TypedData/TypedData.php @@ -70,9 +70,18 @@ public function getString() { } /** + * Implements \Drupal\Core\TypedData\TypedDataInterface::getConstraints(). + */ + public function getConstraints() { + // @todo: Add the typed data manager as proper dependency. + return typed_data()->getConstraints($this->definition); + } + + /** * Implements \Drupal\Core\TypedData\TypedDataInterface::validate(). */ public function validate() { - // TODO: Implement validate() method. + // @todo: Add the typed data manager as proper dependency. + return typed_data()->getValidator()->validate($this); } } diff --git a/core/lib/Drupal/Core/TypedData/TypedDataInterface.php b/core/lib/Drupal/Core/TypedData/TypedDataInterface.php index 27914cc..0fcf36f 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 e978619..ca45e7b 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -11,6 +11,10 @@ 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. @@ -18,6 +22,20 @@ 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; + + /** * An array of typed data property prototypes. * * @var array @@ -76,9 +94,8 @@ public function createInstance($plugin_id, array $configuration, $name = NULL, $ * - 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 @@ -212,4 +229,144 @@ public function getPropertyInstance(ContextAwareInterface $object, $property_nam } return $property; } + + /** + * 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/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php index 68a5701..ae53920 100644 --- a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php +++ b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Tables.php @@ -182,9 +182,9 @@ function addField($field, $type, $langcode) { $next_index_prefix = $relationship_specifier; } // Check for a valid relationship. - if (isset($propertyDefinitions[$relationship_specifier]['constraints']['entity type']) && isset($propertyDefinitions[$relationship_specifier]['settings']['id source'])) { + if (isset($propertyDefinitions[$relationship_specifier]['constraints']['EntityType']) && isset($propertyDefinitions[$relationship_specifier]['settings']['id source'])) { // If it is, use the entity type. - $entity_type = $propertyDefinitions[$relationship_specifier]['constraints']['entity type']; + $entity_type = $propertyDefinitions[$relationship_specifier]['constraints']['EntityType']; $entity_info = entity_get_info($entity_type); // Add the new entity base table using the table and sql column. $join_condition= '%alias.' . $entity_info['entity_keys']['id'] . " = $table.$sql_column"; @@ -194,7 +194,7 @@ function addField($field, $type, $langcode) { $index_prefix .= "$next_index_prefix."; } else { - throw new QueryException(format_string('Invalid specifier @next.', array('@next' => $next))); + throw new QueryException(format_string('Invalid specifier @next.', array('@next' => $relationship_specifier))); } } } 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 f01d440..5f93835 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php @@ -333,7 +333,7 @@ protected function assertIntrospection($entity_type) { $definition = array( 'type' => 'entity', 'constraints' => array( - 'entity type' => $entity_type, + 'EntityType' => $entity_type, ), 'label' => t('Test entity'), ); @@ -432,8 +432,7 @@ protected function assertIterator($entity_type) { } /** - * 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() { // All entity variations have to have the same results. @@ -454,14 +453,15 @@ protected function assertDataStructureInterfaces($entity_type) { $entity_definition = array( 'type' => 'entity', 'constraints' => array( - 'entity type' => $entity_type, + 'EntityType' => $entity_type, ), '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); @@ -504,6 +504,52 @@ public function getContainedStrings(TypedDataInterface $wrapper, $depth, array & } /** + * Tests validation constraints provided by the Entity API. + */ + public function testEntityConstraintValidation() { + $entity = $this->createTestEntity('entity_test'); + $entity->save(); + $entity_definition = array( + 'type' => 'entity', + 'constraints' => array( + 'EntityType' => 'entity_test', + ), + 'label' => t('Test entity'), + ); + $wrapped_entity = typed_data()->create($entity_definition, $entity); + + // 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); + } + + /** * Tests getting processed property values via a computed property. */ public function testComputedProperties() { 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 d88a254..3d2c256 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,94 +31,137 @@ 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'); // Email type. $value = $this->randomString(); @@ -127,17 +177,121 @@ public function testGetAndSet() { $this->assertNull($wrapper->getValue(), 'E-mail wrapper is null-able.'); // 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 7e95a03..f4130db 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 e7a3f40..7f7ab02 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() { static::$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 8c2dcef..73e0f6f 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.