From 78876b2060e631444165279bd41096d9420d9af9 Mon Sep 17 00:00:00 2001 From: Dries Date: Fri, 1 Feb 2013 12:31:52 -0500 Subject: [PATCH] Issue #1845546 by fago, EclipseGc, attiks, effulgentsia: Implement validation for the TypedData API. --- .../Discovery/StaticDiscoveryDecorator.php | 71 ++++ core/lib/Drupal/Core/CoreBundle.php | 5 +- .../Core/Entity/DatabaseStorageController.php | 2 +- core/lib/Drupal/Core/Entity/EntityNG.php | 4 +- .../EntityStorageControllerInterface.php | 4 +- .../Entity/Field/Type/EntityReferenceItem.php | 5 +- .../Core/Entity/Field/Type/EntityWrapper.php | 15 +- .../Drupal/Core/Entity/Field/Type/Field.php | 8 + .../Constraint/BundleConstraint.php | 68 ++++ .../Constraint/BundleConstraintValidator.php | 28 ++ .../Constraint/EntityTypeConstraint.php | 55 +++ .../EntityTypeConstraintValidator.php | 28 ++ .../Constraint/LengthConstraint.php | 39 +++ .../Constraint/PrimitiveTypeConstraint.php | 27 ++ .../PrimitiveTypeConstraintValidator.php | 69 ++++ .../Validation/Constraint/RangeConstraint.php | 38 ++ .../lib/Drupal/Core/TypedData/Type/Binary.php | 8 +- .../Drupal/Core/TypedData/Type/Boolean.php | 7 - core/lib/Drupal/Core/TypedData/Type/Date.php | 3 - .../Drupal/Core/TypedData/Type/Duration.php | 37 +- core/lib/Drupal/Core/TypedData/Type/Email.php | 7 - core/lib/Drupal/Core/TypedData/Type/Float.php | 7 - .../Drupal/Core/TypedData/Type/Integer.php | 7 - .../lib/Drupal/Core/TypedData/Type/String.php | 7 - core/lib/Drupal/Core/TypedData/Type/Uri.php | 7 - core/lib/Drupal/Core/TypedData/TypedData.php | 11 +- .../Core/TypedData/TypedDataInterface.php | 13 + .../Core/TypedData/TypedDataManager.php | 162 ++++++++- .../Core/TypedData/Validation/Metadata.php | 94 +++++ .../TypedData/Validation/MetadataFactory.php | 44 +++ .../Validation/PropertyContainerMetadata.php | 55 +++ .../Core/Validation/ConstraintManager.php | 118 +++++++ .../Core/Validation/DrupalTranslator.php | 92 +++++ .../lib/Drupal/field/Tests/TestItemTest.php | 4 +- .../field_sql_storage/Entity/Tables.php | 6 +- .../system/Tests/Entity/EntityFieldTest.php | 62 +++- .../system/Tests/TypedData/TypedDataTest.php | 324 +++++++++++++----- core/modules/system/system.api.php | 3 + core/modules/system/system.module | 1 + .../Type/TaxonomyTermReferenceItem.php | 2 +- .../Tests/EntityTranslationUITest.php | 2 +- 41 files changed, 1373 insertions(+), 176 deletions(-) create mode 100644 core/lib/Drupal/Component/Plugin/Discovery/StaticDiscoveryDecorator.php create mode 100644 core/lib/Drupal/Core/Plugin/Validation/Constraint/BundleConstraint.php create mode 100644 core/lib/Drupal/Core/Plugin/Validation/Constraint/BundleConstraintValidator.php create mode 100644 core/lib/Drupal/Core/Plugin/Validation/Constraint/EntityTypeConstraint.php create mode 100644 core/lib/Drupal/Core/Plugin/Validation/Constraint/EntityTypeConstraintValidator.php create mode 100644 core/lib/Drupal/Core/Plugin/Validation/Constraint/LengthConstraint.php create mode 100644 core/lib/Drupal/Core/Plugin/Validation/Constraint/PrimitiveTypeConstraint.php create mode 100644 core/lib/Drupal/Core/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php create mode 100644 core/lib/Drupal/Core/Plugin/Validation/Constraint/RangeConstraint.php create mode 100644 core/lib/Drupal/Core/TypedData/Validation/Metadata.php create mode 100644 core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php create mode 100644 core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php create mode 100644 core/lib/Drupal/Core/Validation/ConstraintManager.php create mode 100644 core/lib/Drupal/Core/Validation/DrupalTranslator.php diff --git a/core/lib/Drupal/Component/Plugin/Discovery/StaticDiscoveryDecorator.php b/core/lib/Drupal/Component/Plugin/Discovery/StaticDiscoveryDecorator.php new file mode 100644 index 00000000000..c92ae8dd449 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/Discovery/StaticDiscoveryDecorator.php @@ -0,0 +1,71 @@ +decorated = $decorated; + $this->registerDefinitions = $registerDefinitions; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinition(). + */ + public function getDefinition($base_plugin_id) { + if (isset($this->registerDefinitions)) { + call_user_func($this->registerDefinitions); + } + $this->definitions += $this->decorated->getDefinitions(); + return parent::getDefinition($base_plugin_id); + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinitions(). + */ + public function getDefinitions() { + if (isset($this->registerDefinitions)) { + call_user_func($this->registerDefinitions); + } + $this->definitions += $this->decorated->getDefinitions(); + return parent::getDefinitions(); + } + + /** + * Passes through all unknown calls onto the decorated object + */ + public function __call($method, $args) { + return call_user_func_array(array($this->decorated, $method), $args); + } +} diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 4684695132e..17f07f1ba28 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -149,7 +149,10 @@ class CoreBundle extends Bundle { ->setFactoryClass('Drupal\Core\Database\Database') ->setFactoryMethod('getConnection') ->addArgument('slave'); - $container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager'); + $container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager') + ->addMethodCall('setValidationConstraintManager', array(new Reference('validation.constraint'))); + $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/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index 732119c4630..9092a137249 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -698,7 +698,7 @@ class DatabaseStorageController implements EntityStorageControllerInterface { } } - $bundle = !empty($constraints['bundle']) ? $constraints['bundle'] : FALSE; + $bundle = !empty($constraints['Bundle']) ? $constraints['Bundle'] : FALSE; // Add in per-bundle fields. if (!isset($this->fieldDefinitions[$bundle])) { diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index e16c6fb372a..f8d4300c292 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -218,8 +218,8 @@ class EntityNG extends Entity { 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 2811f1c0e4c..ef69cb6a1a4 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php @@ -125,8 +125,8 @@ interface EntityStorageControllerInterface { * '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 e6e726a6cf8..496a7207c6b 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/EntityReferenceItem.php @@ -38,11 +38,14 @@ class EntityReferenceItem extends FieldItemBase { // @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[$target_type]['entity'] = array( 'type' => 'entity', 'constraints' => array( - 'entity type' => $target_type, + 'EntityType' => $target_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 89179d1a3ba..691868d8228 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 @@ use InvalidArgumentException; * 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 @@ class EntityWrapper extends ContextAwareTypedData implements IteratorAggregate, $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 @@ class EntityWrapper extends ContextAwareTypedData implements IteratorAggregate, * 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 @@ class EntityWrapper extends ContextAwareTypedData implements IteratorAggregate, * 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 34baa6133fb..49418a0592e 100644 --- a/core/lib/Drupal/Core/Entity/Field/Type/Field.php +++ b/core/lib/Drupal/Core/Entity/Field/Type/Field.php @@ -116,6 +116,14 @@ class Field extends ContextAwareTypedData implements IteratorAggregate, FieldInt } } + /** + * Overrides \Drupal\Core\TypedData\TypedData::getConstraints(). + */ + public function getConstraints() { + // Apply the constraints to the list items only. + return array(); + } + /** * Implements \ArrayAccess::offsetExists(). */ 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 00000000000..a9618ae376b --- /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 00000000000..d649ccfb6a9 --- /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 00000000000..391419012f4 --- /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/LengthConstraint.php b/core/lib/Drupal/Core/Plugin/Validation/Constraint/LengthConstraint.php new file mode 100644 index 00000000000..5d02f222098 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Validation/Constraint/LengthConstraint.php @@ -0,0 +1,39 @@ +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 = filter_var($value, FILTER_VALIDATE_FLOAT) !== FALSE; + break; + case Primitive::INTEGER: + $valid = filter_var($value, FILTER_VALIDATE_INT) !== FALSE; + break; + case Primitive::STRING: + // PHP integers, floats or booleans are valid strings also, so we + // cannot use is_string() here. + $valid = is_scalar($value); + break; + case Primitive::URI: + $valid = filter_var($value, FILTER_VALIDATE_URL) ; + 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/Plugin/Validation/Constraint/RangeConstraint.php b/core/lib/Drupal/Core/Plugin/Validation/Constraint/RangeConstraint.php new file mode 100644 index 00000000000..0d27d294b24 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/Validation/Constraint/RangeConstraint.php @@ -0,0 +1,38 @@ +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 @@ class Binary extends TypedData { $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 62a679162d7..6c357c905d6 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 f6c19e0ff11..e32667478da 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Date.php +++ b/core/lib/Drupal/Core/TypedData/Type/Date.php @@ -40,9 +40,6 @@ class Date extends TypedData { } else { $this->value = $value instanceOf DrupalDateTime ? $value : new DrupalDateTime($value); - if ($this->value->hasErrors()) { - throw new InvalidArgumentException("Invalid date format given."); - } } } } diff --git a/core/lib/Drupal/Core/TypedData/Type/Duration.php b/core/lib/Drupal/Core/TypedData/Type/Duration.php index 527fbddf2f6..fa539cf8bbd 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Duration.php +++ b/core/lib/Drupal/Core/TypedData/Type/Duration.php @@ -32,21 +32,30 @@ class Duration extends TypedData { * Overrides TypedData::setValue(). */ public function setValue($value) { - if ($value instanceof DateInterval || !isset($value)) { - $this->value = $value; + // Catch any exceptions thrown due to invalid values being passed. + 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/Email.php b/core/lib/Drupal/Core/TypedData/Type/Email.php index e9a9c5ac37b..314f8f5185e 100644 --- a/core/lib/Drupal/Core/TypedData/Type/Email.php +++ b/core/lib/Drupal/Core/TypedData/Type/Email.php @@ -14,11 +14,4 @@ namespace Drupal\Core\TypedData\Type; */ class Email extends String { - /** - * Implements \Drupal\Core\TypedData\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 fe7c8b399df..482cad12988 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 487e1eb4dc8..eb6336effdf 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 f5402ad27c9..a9d096f97ae 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 3dd893d9bf3..791dec9f613 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 35e168c1618..d6ad4bee29f 100644 --- a/core/lib/Drupal/Core/TypedData/TypedData.php +++ b/core/lib/Drupal/Core/TypedData/TypedData.php @@ -69,10 +69,19 @@ abstract class TypedData implements TypedDataInterface { return (string) $this->getValue(); } + /** + * 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 27914cc540b..0fcf36f0cbf 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataInterface.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataInterface.php @@ -56,8 +56,21 @@ interface TypedDataInterface { */ 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 e978619b32d..8b5306d7b51 100644 --- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php +++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php @@ -11,12 +11,31 @@ use InvalidArgumentException; use Drupal\Component\Plugin\PluginManagerBase; use Drupal\Core\Plugin\Discovery\CacheDecorator; use Drupal\Core\Plugin\Discovery\HookDiscovery; +use Drupal\Core\TypedData\Validation\MetadataFactory; +use Drupal\Core\Validation\ConstraintManager; +use Drupal\Core\Validation\DrupalTranslator; +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; + /** * An array of typed data property prototypes. * @@ -76,9 +95,8 @@ class TypedDataManager extends PluginManagerBase { * - 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 +230,142 @@ class TypedDataManager extends PluginManagerBase { } 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()) + ->setTranslator(new DrupalTranslator()) + ->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() { + 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 00000000000..106f2d08237 --- /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(); + } +} 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 00000000000..2858daf45cd --- /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 Metadata($this->typedData[$property_name], $property_name)); + } + elseif ($this->typedData instanceof ComplexDataInterface) { + return array(new Metadata($this->typedData->get($property_name), $property_name)); + } + else { + throw new \LogicException("There are no known properties."); + } + } +} diff --git a/core/lib/Drupal/Core/Validation/ConstraintManager.php b/core/lib/Drupal/Core/Validation/ConstraintManager.php new file mode 100644 index 00000000000..5177f090b80 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php @@ -0,0 +1,118 @@ +discovery = new AnnotatedClassDiscovery('Validation', 'Constraint'); + $this->discovery = new StaticDiscoveryDecorator($this->discovery, array($this, 'registerDefinitions')); + $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:' . language(LANGUAGE_TYPE_INTERFACE)->langcode); + + $this->factory = new DefaultFactory($this); + } + + /** + * Callback for registering definitions for constraints shipped with Symfony. + * + * @see ConstraintManager::__construct() + */ + public function registerDefinitions() { + $this->discovery->setDefinition('Null', array( + 'label' => t('Null'), + 'class' => '\Symfony\Component\Validator\Constraints\Null', + 'type' => FALSE, + )); + $this->discovery->setDefinition('NotNull', array( + 'label' => t('Not null'), + 'class' => '\Symfony\Component\Validator\Constraints\NotNull', + 'type' => FALSE, + )); + $this->discovery->setDefinition('Blank', array( + 'label' => t('Blank'), + 'class' => '\Symfony\Component\Validator\Constraints\Blank', + 'type' => FALSE, + )); + $this->discovery->setDefinition('NotBlank', array( + 'label' => t('Not blank'), + 'class' => '\Symfony\Component\Validator\Constraints\NotBlank', + 'type' => FALSE, + )); + $this->discovery->setDefinition('Email', array( + 'label' => t('E-mail'), + 'class' => '\Symfony\Component\Validator\Constraints\Email', + 'type' => array('string'), + )); + } + + /** + * 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/DrupalTranslator.php b/core/lib/Drupal/Core/Validation/DrupalTranslator.php new file mode 100644 index 00000000000..870052606f5 --- /dev/null +++ b/core/lib/Drupal/Core/Validation/DrupalTranslator.php @@ -0,0 +1,92 @@ +processParameters($parameters), $this->getOptions($domain, $locale)); + } + + /** + * Implements \Symfony\Component\Translation\TranslatorInterface::transChoice(). + */ + public function transChoice($id, $number, array $parameters = array(), $domain = NULL, $locale = NULL) { + // Violation messages can separated singular and plural versions by "|". + $ids = explode('|', $id); + + if (!isset($ids[1])) { + throw new \InvalidArgumentException(sprintf('The message "%s" cannot be pluralized, because it is missing a plural (e.g. "There is one apple|There are @count apples").', $id)); + } + return format_plural($number, $ids[0], $ids[1], $this->processParameters($parameters), $this->getOptions($domain, $locale)); + } + + /** + * Implements \Symfony\Component\Translation\TranslatorInterface::setLocale(). + */ + public function setLocale($locale) { + $this->locale = $locale; + } + + /** + * Implements \Symfony\Component\Translation\TranslatorInterface::getLocale(). + */ + public function getLocale() { + return $this->locale ? $this->locale : language(LANGUAGE_TYPE_INTERFACE)->langcode; + } + + /** + * Processes the parameters array for use with t(). + */ + protected function processParameters(array $parameters) { + $return = array(); + foreach ($parameters as $key => $value) { + if (is_object($value)) { + // t() does not work will objects being passed as replacement strings. + } + // Check for symfony replacement patterns in the form "{{ name }}". + elseif (strpos($key, '{{ ') === 0 && strrpos($key, ' }}') == strlen($key) - 3) { + // Transform it into a Drupal pattern using the format %name. + $key = '%' . substr($key, 3, strlen($key) - 6); + $return[$key] = $value; + } + else { + $return[$key] = $value; + } + } + return $return; + } + + /** + * Returns options suitable for use with t(). + */ + protected function getOptions($domain = NULL, $locale = NULL) { + // We do not support domains, so we ignore this parameter. + // If locale is left NULL, t() will default to the interface language. + $locale = isset($locale) ? $locale : $this->locale; + return array('langcode' => $locale); + } +} diff --git a/core/modules/field/lib/Drupal/field/Tests/TestItemTest.php b/core/modules/field/lib/Drupal/field/Tests/TestItemTest.php index a1cce1668c4..4873eb73577 100644 --- a/core/modules/field/lib/Drupal/field/Tests/TestItemTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/TestItemTest.php @@ -56,7 +56,7 @@ class TestItemTest extends FieldItemUnitTestBase { public function testTestItem() { // Verify entity creation. $entity = entity_create('entity_test', array()); - $value = $this->randomName(); + $value = rand(1, 10); $entity->field_test = $value; $entity->name->value = $this->randomName(); $entity->save(); @@ -70,7 +70,7 @@ class TestItemTest extends FieldItemUnitTestBase { $this->assertEqual($entity->field_test[0]->value, $value); // Verify changing the field value. - $new_value = $this->randomName(); + $new_value = rand(1, 10); $entity->field_test->value = $new_value; $this->assertEqual($entity->field_test->value, $new_value); 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 428d778aed8..09c8fda4fe6 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 @@ -184,9 +184,9 @@ class Tables { $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"; @@ -196,7 +196,7 @@ class Tables { $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 2a98ffafa9a..e3ad6fe746e 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFieldTest.php @@ -333,9 +333,9 @@ class EntityFieldTest extends WebTestBase { $definition = array( 'type' => 'entity', 'constraints' => array( - 'entity type' => $entity_type, + 'EntityType' => $entity_type, ), - 'label' => t('Test entity'), + 'label' => 'Test entity', ); $wrapped_entity = typed_data()->create($definition); $definitions = $wrapped_entity->getPropertyDefinitions($definition); @@ -432,8 +432,7 @@ class EntityFieldTest extends WebTestBase { } /** - * 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 @@ class EntityFieldTest extends WebTestBase { $entity_definition = array( 'type' => 'entity', 'constraints' => array( - 'entity type' => $entity_type, + 'EntityType' => $entity_type, ), - 'label' => t('Test entity'), + 'label' => '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); @@ -503,6 +503,52 @@ class EntityFieldTest extends WebTestBase { } } + /** + * 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' => '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' => 'Test node', + ); + $wrapped_entity = typed_data()->create($entity_definition, $node); + + $violations = $wrapped_entity->validate(); + $this->assertEqual($violations->count(), 1); + + $node->type = 'article'; + $node->save(); + $wrapped_entity->setValue($node); + $violations = $wrapped_entity->validate(); + $this->assertEqual($violations->count(), 0); + } + /** * Tests getting processed property values via a computed property. */ 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 d88a254cafc..a401183343c 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 @@ use DateInterval; */ 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,120 +31,277 @@ class TypedDataTest extends WebTestBase { ); } + 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(); - $wrapper = $this->createTypedData(array('type' => 'email'), $value); - $this->assertIdentical($wrapper->getValue(), $value, 'E-mail value was fetched.'); - + $typed_data = $this->createTypedData(array('type' => 'email'), $value); + $this->assertIdentical($typed_data->getValue(), $value, 'E-mail value was fetched.'); $new_value = 'test@example.com'; - $wrapper->setValue($new_value); - $this->assertIdentical($wrapper->getValue(), $new_value, 'E-mail value was changed.'); - $this->assertTrue(is_string($wrapper->getString()), 'E-mail value was converted to string'); - - $wrapper->setValue(NULL); - $this->assertNull($wrapper->getValue(), 'E-mail wrapper is null-able.'); + $typed_data->setValue($new_value); + $this->assertIdentical($typed_data->getValue(), $new_value, 'E-mail value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'E-mail value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'E-mail wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalidATexample.com'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); // 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); + + // Test translating violation messages. + $message = t('This value should be %limit or more.', array('%limit' => 5)); + $this->assertEqual($violations[0]->getMessage(), $message, 'Translated violation message retrieved.'); + $this->assertEqual($violations[0]->getPropertyPath(), ''); + $this->assertIdentical($violations[0]->getRoot(), $integer, 'Root object returned.'); + + // Test translating violation messages when pluralization is used. + $definition = array( + 'type' => 'string', + 'constraints' => array( + 'Length' => array('min' => 10), + ), + ); + $violations = $this->typedData->create($definition, "short")->validate(); + $this->assertEqual($violations->count(), 1); + $message = t('This value is too short. It should have %limit characters or more.', array('%limit' => 10)); + $this->assertEqual($violations[0]->getMessage(), $message, 'Translated violation message retrieved.'); + + // 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 79d6950d189..2c97b0a694c 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -163,6 +163,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() @@ -174,6 +176,7 @@ function hook_data_type_info() { 'label' => t('Email'), 'class' => '\Drupal\email\Type\Email', 'primitive type' => \Drupal\Core\TypedData\Primitive::String, + 'constraints' => array('Email' => array()), ), ); } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 469a7a05d74..e5367ea50d5 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2224,6 +2224,7 @@ function system_data_type_info() { 'label' => t('Email'), 'class' => '\Drupal\Core\TypedData\Type\Email', 'primitive type' => Primitive::STRING, + 'constraints' => array('Email' => array()), ), 'binary' => array( 'label' => t('Binary'), diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Type/TaxonomyTermReferenceItem.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Type/TaxonomyTermReferenceItem.php index e7a3f40fa3f..7f7ab021c01 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 @@ class TaxonomyTermReferenceItem extends FieldItemBase { 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 5fe8e896637..f865be00cde 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 @@ -39,7 +39,7 @@ abstract class EntityTranslationUITest extends EntityTranslationTestBase { $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.