Issue #1845546 by fago, EclipseGc, attiks, effulgentsia: Implement validation for the TypedData API.

8.0.x
Dries 2013-02-01 12:31:52 -05:00
parent fb08e2f281
commit 78876b2060
41 changed files with 1373 additions and 176 deletions

View File

@ -0,0 +1,71 @@
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\Discovery\StaticDiscoveryDecorator.
*/
namespace Drupal\Component\Plugin\Discovery;
/**
* A decorator that allows manual registration of undiscoverable definitions.
*/
class StaticDiscoveryDecorator extends StaticDiscovery {
/**
* The Discovery object being decorated.
*
* @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $decorated;
/**
* A callback or closure used for registering additional definitions.
*
* @var \Callable
*/
protected $registerDefinitions;
/**
* Constructs a \Drupal\Component\Plugin\Discovery\StaticDiscoveryDecorator object.
*
* @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated
* The discovery object that is being decorated.
* @param \Callable $registerDefinitions
* (optional) A callback or closure used for registering additional
* definitions.
*/
public function __construct(DiscoveryInterface $decorated, $registerDefinitions = NULL) {
$this->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);
}
}

View File

@ -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')

View File

@ -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])) {

View File

@ -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;

View File

@ -125,8 +125,8 @@ interface EntityStorageControllerInterface {
* 'bundle' key. For example:
* @code
* array(
* 'entity type' => 'node',
* 'bundle' => 'article',
* 'EntityType' => 'node',
* 'Bundle' => 'article',
* )
* @endcode
*

View File

@ -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'),

View File

@ -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();
}
}

View File

@ -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().
*/

View File

@ -0,0 +1,68 @@
<?php
/**
* @file
* Contains \Drupal\Core\Validation\Constraint\BundleConstraint.
*/
namespace Drupal\Core\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
/**
* Checks if a value is a valid entity type.
*
* @todo: Move this below the entity core component.
*
* @Plugin(
* id = "Bundle",
* label = @Translation("Bundle", context = "Validation"),
* type = "entity"
* )
*/
class BundleConstraint extends Constraint {
/**
* The default violation message.
*
* @var string
*/
public $message = 'The entity must be of bundle %bundle.';
/**
* The bundle option.
*
* @var string|array
*/
public $bundle;
/**
* Gets the bundle option as array.
*
* @return array
*/
public function getBundleOption() {
// Support passing the bundle as string, but force it to be an array.
if (!is_array($this->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');
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Validation\Constraint\BundleConstraintValidator.
*/
namespace Drupal\Core\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the Bundle constraint.
*/
class BundleConstraintValidator extends ConstraintValidator {
/**
* Implements \Symfony\Component\Validator\ConstraintValidatorInterface::validate().
*/
public function validate($typed_data, Constraint $constraint) {
$entity = isset($typed_data) ? $typed_data->getValue() : FALSE;
if (!empty($entity) && !in_array($entity->bundle(), $constraint->getBundleOption())) {
$this->context->addViolation($constraint->message, array('%bundle', implode(', ', $constraint->getBundleOption())));
}
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\Core\Validation\Constraint\EntityTypeConstraint.
*/
namespace Drupal\Core\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
/**
* Checks if a value is a valid entity type.
*
* @todo: Move this below the entity core component.
*
* @Plugin(
* id = "EntityType",
* label = @Translation("Entity type", context = "Validation"),
* type = "entity"
* )
*/
class EntityTypeConstraint extends Constraint {
/**
* The default violation message.
*
* @var string
*/
public $message = 'The entity must be of type %type.';
/**
* The entity type option.
*
* @var string
*/
public $type;
/**
* Overrides Constraint::getDefaultOption().
*/
public function getDefaultOption() {
return 'type';
}
/**
* Overrides Constraint::getRequiredOptions().
*/
public function getRequiredOptions() {
return array('type');
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Validation\Constraint\EntityTypeConstraintValidator.
*/
namespace Drupal\Core\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the EntityType constraint.
*/
class EntityTypeConstraintValidator extends ConstraintValidator {
/**
* Implements \Symfony\Component\Validator\ConstraintValidatorInterface::validate().
*/
public function validate($typed_data, Constraint $constraint) {
$entity = isset($typed_data) ? $typed_data->getValue() : FALSE;
if (!empty($entity) && $entity->entityType() != $constraint->type) {
$this->context->addViolation($constraint->message, array('%type', $constraint->type));
}
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Validation\Constraint\LengthConstraint.
*/
namespace Drupal\Core\Plugin\Validation\Constraint;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Symfony\Component\Validator\Constraints\Length;
/**
* Length constraint.
*
* Overrides the symfony constraint to use Drupal-style replacement patterns.
*
* @todo: Move this below the TypedData core component.
*
* @Plugin(
* id = "Length",
* label = @Translation("Length", context = "Validation"),
* type = { "string" }
* )
*/
class LengthConstraint extends Length {
public $maxMessage = 'This value is too long. It should have %limit character or less.|This value is too long. It should have %limit characters or less.';
public $minMessage = 'This value is too short. It should have %limit character or more.|This value is too short. It should have %limit characters or more.';
public $exactMessage = 'This value should have exactly %limit character.|This value should have exactly %limit characters.';
/**
* Overrides Range::validatedBy().
*/
public function validatedBy() {
return '\Symfony\Component\Validator\Constraints\LengthValidator';
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Validation\Constraint\PrimitiveTypeConstraint.
*/
namespace Drupal\Core\Plugin\Validation\Constraint;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Symfony\Component\Validator\Constraints\Type as SymfonyConstraint;
/**
* Supports validating all primitive types.
*
* @todo: Move this below the TypedData core component.
*
* @Plugin(
* id = "PrimitiveType",
* label = @Translation("Primitive type", context = "Validation")
* )
*/
class PrimitiveTypeConstraint extends SymfonyConstraint {
public $message = 'This value should be of type %type.';
}

View File

@ -0,0 +1,69 @@
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Validation\Constraint\PrimitiveTypeConstraintValidator.
*/
namespace Drupal\Core\Plugin\Validation\Constraint;
use DateInterval;
use Drupal\Core\TypedData\Primitive;
use Drupal\Core\Datetime\DrupalDateTime;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the PrimitiveType constraint.
*/
class PrimitiveTypeConstraintValidator extends ConstraintValidator {
/**
* Implements \Symfony\Component\Validator\ConstraintValidatorInterface::validate().
*/
public function validate($value, Constraint $constraint) {
if (!isset($value)) {
return;
}
switch ($constraint->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,
));
}
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Validation\Constraint\RangeConstraint.
*/
namespace Drupal\Core\Plugin\Validation\Constraint;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Symfony\Component\Validator\Constraints\Range;
/**
* Range constraint.
*
* Overrides the symfony constraint to use Drupal-style replacement patterns.
*
* @todo: Move this below the TypedData core component.
*
* @Plugin(
* id = "Range",
* label = @Translation("Range", context = "Validation"),
* type = { "integer", "float" }
* )
*/
class RangeConstraint extends Range {
public $minMessage = 'This value should be %limit or more.';
public $maxMessage = 'This value should be %limit or less.';
/**
* Overrides Range::validatedBy().
*/
public function validatedBy() {
return '\Symfony\Component\Validator\Constraints\RangeValidator';
}
}

View File

@ -40,7 +40,7 @@ class Binary extends TypedData {
// 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 @@ 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;
}
}

View File

@ -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;
}
}

View File

@ -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.");
}
}
}
}

View File

@ -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;
}
}

View File

@ -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.
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -0,0 +1,94 @@
<?php
/**
* @file
* Contains \Drupal\Core\TypedData\Validation\Metadata.
*/
namespace Drupal\Core\TypedData\Validation;
use Drupal\Core\TypedData\TypedDataInterface;
use Symfony\Component\Validator\ValidationVisitorInterface;
use Symfony\Component\Validator\PropertyMetadataInterface;
/**
* Typed data implementation of the validator MetadataInterface.
*/
class Metadata implements PropertyMetadataInterface {
/**
* The name of the property, or empty if this is the root.
*
* @var string
*/
protected $name;
/**
* The typed data object the metadata is about.
*
* @var \Drupal\Core\TypedData\TypedDataInterface
*/
protected $typedData;
/**
* The metadata factory used.
*
* @var \Drupal\Core\TypedData\Validation\MetadataFactory
*/
protected $factory;
/**
* Constructs the object.
*
* @param \Drupal\Core\TypedData\TypedDataInterface $typed_data
* The typed data object the metadata is about.
* @param $name
* The name of the property to get metadata for. Leave empty, if
* the data is the root of the typed data tree.
* @param \Drupal\Core\TypedData\Validation\MetadataFactory $factory
* The factory to use for instantiating property metadata.
*/
public function __construct(TypedDataInterface $typed_data, $name = '', MetadataFactory $factory) {
$this->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();
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* @file
* Contains \Drupal\Core\TypedData\Validation\MetadataFactory.
*/
namespace Drupal\Core\TypedData\Validation;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Symfony\Component\Validator\MetadataFactoryInterface;
/**
* Typed data implementation of the validator MetadataFactoryInterface.
*/
class MetadataFactory implements MetadataFactoryInterface {
/**
* Implements MetadataFactoryInterface::getMetadataFor().
*
* @param \Drupal\Core\TypedData\TypedDataInterface $typed_data
* Some typed data object containing the value to validate.
* @param $name
* (optional) The name of the property to get metadata for. Leave empty, if
* the data is the root of the typed data tree.
*/
public function getMetadataFor($typed_data, $name = '') {
if (!$typed_data instanceof TypedDataInterface) {
throw new \InvalidArgumentException('The passed value must be a typed data object.');
}
$is_container = $typed_data instanceof ComplexDataInterface || $typed_data instanceof ListInterface;
$class = '\Drupal\Core\TypedData\Validation\\' . ($is_container ? 'PropertyContainerMetadata' : 'Metadata');
return new $class($typed_data, $name, $this);
}
/**
* Implements MetadataFactoryInterface::hasMetadataFor().
*/
public function hasMetadataFor($value) {
return $value instanceof TypedDataInterface;
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\Core\TypedData\Validation\PropertyContainerMetadata.
*/
namespace Drupal\Core\TypedData\Validation;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\ListInterface;
use Symfony\Component\Validator\PropertyMetadataContainerInterface;
use Symfony\Component\Validator\ValidationVisitorInterface;
/**
* Typed data implementation of the validator MetadataInterface.
*/
class PropertyContainerMetadata extends Metadata implements PropertyMetadataContainerInterface {
/**
* Overrides Metadata::accept().
*/
public function accept(ValidationVisitorInterface $visitor, $typed_data, $group, $propertyPath) {
// To let all constraints properly handle empty structures, pass on NULL
// if the data structure is empty. That way existing NotNull or NotBlank
// constraints work as expected.
if ($typed_data->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.");
}
}
}

View File

@ -0,0 +1,118 @@
<?php
/**
* @file
* Contains \Drupal\Core\Validation\ConstraintManager.
*/
namespace Drupal\Core\Validation;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Component\Plugin\Discovery\StaticDiscoveryDecorator;
use Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator;
use Drupal\Component\Plugin\Discovery\ProcessDecorator;
use Drupal\Core\Plugin\Discovery\AlterDecorator;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Core\Plugin\Discovery\CacheDecorator;
/**
* Constraint plugin manager.
*
* Manages validation constraints based upon
* \Symfony\Component\Validator\Constraint, whereas Symfony constraints are
* added in manually during construction. Constraint options are passed on as
* plugin configuration during plugin instantiation.
*
* While core does not prefix constraint plugins, modules have to prefix them
* with the module name in order to avoid any naming conflicts. E.g. a "profile"
* module would have to prefix any constraints with "Profile".
*
* Constraint plugins may specify data types to which support is limited via the
* 'type' key of plugin definitions. Valid values are any types registered via
* the typed data API, or an array of multiple type names. For supporting all
* types FALSE may be specified. The key defaults to an empty array, i.e. no
* types are supported.
*/
class ConstraintManager extends PluginManagerBase {
/**
* Overrides \Drupal\Component\Plugin\PluginManagerBase::__construct().
*/
public function __construct() {
$this->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;
}
}

View File

@ -0,0 +1,92 @@
<?php
/**
* @file
* Contains \Drupal\Core\Validation\DrupalTranslator.
*/
namespace Drupal\Core\Validation;
use Symfony\Component\Translation\TranslatorInterface;
/**
* Translates strings using Drupal's translation system.
*
* This class is used by the Symfony validator to translate violation messages.
*/
class DrupalTranslator implements TranslatorInterface {
/**
* The locale used for translating.
*
* @var string
*/
protected $locale;
/**
* Implements \Symfony\Component\Translation\TranslatorInterface::trans().
*/
public function trans($id, array $parameters = array(), $domain = NULL, $locale = NULL) {
return t($id, $this->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);
}
}

View File

@ -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);

View File

@ -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)));
}
}
}

View File

@ -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.
*/

View File

@ -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);
}
}

View File

@ -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()),
),
);
}

View File

@ -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'),

View File

@ -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'),

View File

@ -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.