Commit 78876b20 authored by Dries's avatar Dries

Issue #1845546 by fago, EclipseGc, attiks, effulgentsia: Implement validation...

Issue #1845546 by fago, EclipseGc, attiks, effulgentsia: Implement validation for the TypedData API.
parent fb08e2f2
<?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);
}
}
......@@ -149,7 +149,10 @@ public function build(ContainerBuilder $container) {
->setFactoryClass('Drupal\Core\Database\Database')
->setFactoryMethod('getConnection')
->addArgument('slave');
$container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager');
$container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager')
->addMethodCall('setValidationConstraintManager', array(new Reference('validation.constraint')));
$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')
......
......@@ -698,7 +698,7 @@ public function getFieldDefinitions(array $constraints) {
}
}
$bundle = !empty($constraints['bundle']) ? $constraints['bundle'] : FALSE;
$bundle = !empty($constraints['Bundle']) ? $constraints['Bundle'] : FALSE;
// Add in per-bundle fields.
if (!isset($this->fieldDefinitions[$bundle])) {
......
......@@ -218,8 +218,8 @@ public function getPropertyDefinition($name) {
public function getPropertyDefinitions() {
if (!isset($this->fieldDefinitions)) {
$this->fieldDefinitions = drupal_container()->get('plugin.manager.entity')->getStorageController($this->entityType)->getFieldDefinitions(array(
'entity type' => $this->entityType,
'bundle' => $this->bundle,
'EntityType' => $this->entityType,
'Bundle' => $this->bundle,
));
}
return $this->fieldDefinitions;
......
......@@ -125,8 +125,8 @@ public function save(EntityInterface $entity);
* 'bundle' key. For example:
* @code
* array(
* 'entity type' => 'node',
* 'bundle' => 'article',
* 'EntityType' => 'node',
* 'Bundle' => 'article',
* )
* @endcode
*
......
......@@ -38,11 +38,14 @@ public function getPropertyDefinitions() {
// @todo: Lookup the entity type's ID data type and use it here.
'type' => 'integer',
'label' => t('Entity ID'),
'constraints' => array(
'Range' => array('min' => 0),
),
);
static::$propertyDefinitions[$target_type]['entity'] = array(
'type' => 'entity',
'constraints' => array(
'entity type' => $target_type,
'EntityType' => $target_type,
),
'label' => t('Entity'),
'description' => t('The referenced entity'),
......
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Entity\Field\Type;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityNG;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\ContextAwareTypedData;
......@@ -29,8 +30,8 @@
* an 'entity type' constraint is specified.
*
* Supported constraints (below the definition's 'constraints' key) are:
* - entity type: The entity type.
* - bundle: The bundle or an array of possible bundles.
* - EntityType: The entity type.
* - Bundle: The bundle or an array of possible bundles.
*
* Supported settings (below the definition's 'settings' key) are:
* - id source: If used as computed property, the ID property used to load
......@@ -57,7 +58,7 @@ class EntityWrapper extends ContextAwareTypedData implements IteratorAggregate,
*/
public function __construct(array $definition, $name = NULL, ContextAwareInterface $parent = NULL) {
parent::__construct($definition, $name, $parent);
$this->entityType = isset($this->definition['constraints']['entity type']) ? $this->definition['constraints']['entity type'] : NULL;
$this->entityType = isset($this->definition['constraints']['EntityType']) ? $this->definition['constraints']['EntityType'] : NULL;
}
/**
......@@ -89,7 +90,7 @@ public function setValue($value) {
$this->entityType = $value->entityType();
$value = $value->id();
}
elseif (isset($value) && !(is_scalar($value) && !empty($this->definition['constraints']['entity type']))) {
elseif (isset($value) && !(is_scalar($value) && !empty($this->definition['constraints']['EntityType']))) {
throw new InvalidArgumentException('Value is not a valid entity.');
}
// Now update the value in the source or the local id property.
......@@ -116,7 +117,9 @@ public function getString() {
* Implements \IteratorAggregate::getIterator().
*/
public function getIterator() {
if ($entity = $this->getValue()) {
// @todo: Remove check for EntityNG once all entity types are converted.
$entity = $this->getValue();
if ($entity && $entity instanceof EntityNG) {
return $entity->getIterator();
}
return new ArrayIterator(array());
......@@ -193,6 +196,6 @@ public function setPropertyValues($values) {
* Implements \Drupal\Core\TypedData\ComplexDataInterface::isEmpty().
*/
public function isEmpty() {
return (bool) $this->getValue();
return !$this->getValue();
}
}
......@@ -116,6 +116,14 @@ public function getString() {
}
}
/**
* Overrides \Drupal\Core\TypedData\TypedData::getConstraints().
*/
public function getConstraints() {
// Apply the constraints to the list items only.
return array();
}
/**
* Implements \ArrayAccess::offsetExists().
*/
......
<?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');
}
}
<?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())));
}
}
}
<?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');
}
}
<?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));
}
}
}
<?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';
}
}
<?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.';
}
<?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,
));
}
}
}
<?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';
}
}
......@@ -40,7 +40,7 @@ public function getValue() {
// If the value has been set by (absolute) stream resource URI, access the
// resource now.
if (!isset($this->handle) && isset($this->uri)) {
$this->handle = fopen($this->uri, 'rb');
$this->handle = is_readable($this->uri) ? fopen($this->uri, 'rb') : FALSE;
}
return $this->handle;
}
......@@ -55,16 +55,14 @@ public function setValue($value) {
$this->handle = NULL;
$this->uri = NULL;
}
elseif (is_resource($value)) {
$this->handle = $value;
}
elseif (is_string($value)) {
// Note: For performance reasons we store the given URI and access the
// resource upon request. See Binary::getValue()
$this->uri = $value;
$this->handle = NULL;
}
else {
throw new InvalidArgumentException("Invalid value for binary data given.");
$this->handle = $value;
}
}
......
......@@ -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;
}
}
......@@ -40,9 +40,6 @@ public function setValue($value) {
}
else {
$this->value = $value instanceOf DrupalDateTime ? $value : new DrupalDateTime($value);
if ($this->value->hasErrors()) {
throw new InvalidArgumentException("Invalid date format given.");
}
}
}
}
......@@ -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;
}
}
......
......@@ -14,11 +14,4 @@
*/
class Email extends String {
/**
* Implements \Drupal\Core\TypedData\TypedDataInterface::validate().
*/
public function validate() {
// @todo Implement validate() method.
}
}
......@@ -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;
}
}
......@@ -23,11 +23,4 @@ class Integer extends TypedData {
* @var integer
*/
protected $value;