Commit 979a252c authored by alexpott's avatar alexpott

Issue #2247085 by larowlan, Berdir: Constraints cannot be added to configurable fields

parent 7ed8d75d
......@@ -297,6 +297,10 @@ public function setQueryable($queryable) {
/**
* Sets constraints for a given field item property.
*
* Note: this overwrites any existing property constraints. If you need to
* add to the existing constraints, use
* \Drupal\Core\Field\BaseFieldDefinition::addPropertyConstraints()
*
* @param string $name
* The name of the property to set constraints for.
* @param array $constraints
......@@ -312,6 +316,51 @@ public function setPropertyConstraints($name, array $constraints) {
return $this;
}
/**
* Adds constraints for a given field item property.
*
* Adds a constraint to a property of a base field item. e.g.
* @code
* // Limit the field item's value property to the range 0 through 10.
* // e.g. $node->size->value.
* $field->addPropertyConstraints('value', [
* 'Range' => [
* 'min' => 0,
* 'max' => 10,
* ]
* ]);
* @endcode
*
* If you want to add a validation constraint that applies to the
* \Drupal\Core\Field\FieldItemList, use BaseFieldDefinition::addConstraint()
* instead.
*
* Note: passing a new set of options for an existing property constraint will
* overwrite with the new options.
*
* @param string $name
* The name of the property to set constraints for.
* @param array $constraints
* The constraints to set.
*
* @return static
* The object itself for chaining.
*
* @see \Drupal\Core\Field\BaseFieldDefinition::addConstraint()
*/
public function addPropertyConstraints($name, array $constraints) {
$item_constraints = $this->getItemDefinition()->getConstraint('ComplexData') ?: [];
if (isset($item_constraints[$name])) {
// Add the new property constraints, overwriting as required.
$item_constraints[$name] = $constraints + $item_constraints[$name];
}
else {
$item_constraints[$name] = $constraints;
}
$this->getItemDefinition()->addConstraint('ComplexData', $item_constraints);
return $this;
}
/**
* Sets the display options for the field in forms or rendered entities.
*
......
......@@ -190,6 +190,13 @@ abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigIn
*/
protected $bundleRenameAllowed = FALSE;
/**
* Array of constraint options keyed by constraint plugin ID.
*
* @var array
*/
protected $constraints = [];
/**
* {@inheritdoc}
*/
......@@ -430,7 +437,7 @@ public function getClass() {
* {@inheritdoc}
*/
public function getConstraints() {
return \Drupal::typedDataManager()->getDefaultConstraints($this);
return \Drupal::typedDataManager()->getDefaultConstraints($this) + $this->constraints;
}
/**
......@@ -482,4 +489,44 @@ public function getConfig($bundle) {
return $this;
}
/**
* {@inheritdoc}
*/
public function setConstraints(array $constraints) {
$this->constraints = $constraints;
}
/**
* {@inheritdoc}
*/
public function addConstraint($constraint_name, $options = NULL) {
$this->constraints[$constraint_name] = $options;
}
/**
* {@inheritdoc}
*/
public function setPropertyConstraints($name, array $constraints) {
$item_constraints = $this->getItemDefinition()->getConstraints();
$item_constraints['ComplexData'][$name] = $constraints;
$this->getItemDefinition()->setConstraints($item_constraints);
return $this;
}
/**
* {@inheritdoc}
*/
public function addPropertyConstraints($name, array $constraints) {
$item_constraints = $this->getItemDefinition()->getConstraint('ComplexData') ?: [];
if (isset($item_constraints[$name])) {
// Add the new property constraints, overwriting as required.
$item_constraints[$name] = $constraints + $item_constraints[$name];
}
else {
$item_constraints[$name] = $constraints;
}
$this->getItemDefinition()->addConstraint('ComplexData', $item_constraints);
return $this;
}
}
......@@ -65,4 +65,131 @@ public function allowBundleRename();
*/
public function setDefaultValue($value);
/**
* Sets constraints for a given field item property.
*
* Note: this overwrites any existing property constraints. If you need to
* add to the existing constraints, use
* \Drupal\Core\Field\FieldConfigInterface::addPropertyConstraints()
*
* Note that constraints added via this method are not stored in configuration
* and as such need to be added at runtime using
* hook_entity_bundle_field_info_alter().
*
* @param string $name
* The name of the property to set constraints for.
* @param array $constraints
* The constraints to set.
*
* @return static
* The object itself for chaining.
*
* @see hook_entity_bundle_field_info_alter()
*/
public function setPropertyConstraints($name, array $constraints);
/**
* Adds constraints for a given field item property.
*
* Adds a constraint to a property of a field item. e.g.
* @code
* // Limit the field item's value property to the range 0 through 10.
* // e.g. $node->field_how_many->value.
* $field->addPropertyConstraints('value', [
* 'Range' => [
* 'min' => 0,
* 'max' => 10,
* ]
* ]);
* @endcode
*
* If you want to add a validation constraint that applies to the
* \Drupal\Core\Field\FieldItemList, use FieldConfigInterface::addConstraint()
* instead.
*
* Note: passing a new set of options for an existing property constraint will
* overwrite with the new options.
*
* Note that constraints added via this method are not stored in configuration
* and as such need to be added at runtime using
* hook_entity_bundle_field_info_alter().
*
* @param string $name
* The name of the property to set constraints for.
* @param array $constraints
* The constraints to set.
*
* @return static
* The object itself for chaining.
*
* @see \Drupal\Core\Field\FieldConfigInterface::addConstraint()
* @see hook_entity_bundle_field_info_alter()
*/
public function addPropertyConstraints($name, array $constraints);
/**
* Adds a validation constraint to the FieldItemList.
*
* Note: If you wish to apply a constraint to just a property of a FieldItem
* use \Drupal\Core\Field\FieldConfigInterface::addPropertyConstraints()
* instead.
* @code
* // Add a constraint to the 'field_username' FieldItemList.
* // e.g. $node->field_username
* $fields['field_username']->addConstraint('UserNameUnique', []);
* @endcode
*
* If you wish to apply a constraint to a \Drupal\Core\Field\FieldItem instead
* of a property or FieldItemList, you can use the
* \Drupal\Core\Field\FieldConfigBase::getItemDefinition() method.
* @code
* // Add a constraint to the 'field_entity_reference' FieldItem (entity
* // reference item).
* $fields['field_entity_reference']->getItemDefinition()->addConstraint('MyCustomFieldItemValidationPlugin', []);
* @endcode
*
* See \Drupal\Core\TypedData\DataDefinitionInterface::getConstraints() for
* details.
*
* Note that constraints added via this method are not stored in configuration
* and as such need to be added at runtime using
* hook_entity_bundle_field_info_alter().
*
* @param string $constraint_name
* The name of the constraint to add, i.e. its plugin id.
* @param array|null $options
* The constraint options as required by the constraint plugin, or NULL.
*
* @return static
* The object itself for chaining.
*
* @see \Drupal\Core\Field\FieldItemList
* @see \Drupal\Core\Field\FieldConfigInterface::addPropertyConstraints()
* @see hook_entity_bundle_field_info_alter()
*/
public function addConstraint($constraint_name, $options = NULL);
/**
* Sets the array of validation constraints for the FieldItemList.
*
* NOTE: This will overwrite any previously set constraints. In most cases
* FieldConfigInterface::addConstraint() should be used instead.
*
* Note that constraints added via this method are not stored in configuration
* and as such need to be added at runtime using
* hook_entity_bundle_field_info_alter().
*
* @param array $constraints
* The array of constraints. See
* \Drupal\Core\TypedData\TypedDataManager::getConstraints() for details.
*
* @return $this
*
* @see \Drupal\Core\TypedData\DataDefinition::addConstraint()
* @see \Drupal\Core\TypedData\DataDefinition::getConstraints()
* @see \Drupal\Core\Field\FieldItemList
* @see hook_entity_bundle_field_info_alter()
*/
public function setConstraints(array $constraints);
}
......@@ -276,19 +276,7 @@ public function getConstraint($constraint_name) {
}
/**
* Sets the array of validation constraints.
*
* NOTE: This will override any previously set constraints. In most cases
* DataDefinition::addConstraint() should be used instead.
*
* @param array $constraints
* The array of constraints. See
* \Drupal\Core\TypedData\TypedDataManager::getConstraints() for details.
*
* @return $this
*
* @see \Drupal\Core\TypedData\DataDefinition::addConstraint()
* @see \Drupal\Core\TypedData\DataDefinition::getConstraints()
* {@inheritdoc}
*/
public function setConstraints(array $constraints) {
$this->definition['constraints'] = $constraints;
......@@ -296,18 +284,7 @@ public function setConstraints(array $constraints) {
}
/**
* Adds a validation constraint.
*
* See \Drupal\Core\TypedData\DataDefinitionInterface::getConstraints() for
* details.
*
* @param string $constraint_name
* The name of the constraint to add, i.e. its plugin id.
* @param array|null $options
* The constraint options as required by the constraint plugin, or NULL.
*
* @return static
* The object itself for chaining.
* {@inheritdoc}
*/
public function addConstraint($constraint_name, $options = NULL) {
$this->definition['constraints'][$constraint_name] = $options;
......
......@@ -203,4 +203,20 @@ public function getConstraints();
*/
public function getConstraint($constraint_name);
/**
* Adds a validation constraint.
*
* See \Drupal\Core\TypedData\DataDefinitionInterface::getConstraints() for
* details.
*
* @param string $constraint_name
* The name of the constraint to add, i.e. its plugin id.
* @param array|null $options
* The constraint options as required by the constraint plugin, or NULL.
*
* @return static
* The object itself for chaining.
*/
public function addConstraint($constraint_name, $options = NULL);
}
......@@ -68,6 +68,10 @@ function setUp() {
* Test the creation of a field.
*/
function testCreateField() {
// Set a state flag so that field_test.module knows to add an in-memory
// constraint for this field.
\Drupal::state()->set('field_test_add_constraint', $this->fieldStorage->getName());
/** @var \Drupal\Core\Field\FieldConfigInterface $field */
$field = entity_create('field_config', $this->fieldDefinition);
$field->save();
......@@ -88,6 +92,17 @@ function testCreateField() {
// Check that the denormalized 'field_type' was properly written.
$this->assertEqual($config['field_type'], $this->fieldStorageDefinition['type']);
// Test constraints are applied. A Range constraint is added dynamically to
// limit the field to values between 0 and 32.
// @see field_test_entity_bundle_field_info_alter()
$this->doFieldValidationTests();
// Test FieldConfigBase::setPropertyConstraints().
\Drupal::state()->set('field_test_set_constraint', $this->fieldStorage->getName());
\Drupal::state()->set('field_test_add_constraint', FALSE);
\Drupal::entityManager()->clearCachedFieldDefinitions();
$this->doFieldValidationTests();
// Guarantee that the field/bundle combination is unique.
try {
entity_create('field_config', $this->fieldDefinition)->save();
......@@ -219,4 +234,24 @@ function testDeleteFieldCrossDeletion() {
$this->assertFalse(FieldStorageConfig::loadByName('entity_test', $field_storage->field_name));
}
/**
* Tests configurable field validation.
*
* @see field_test_entity_bundle_field_info_alter()
*/
protected function doFieldValidationTests() {
$entity = entity_create('entity_test');
$entity->set($this->fieldStorage->getName(), 1);
$violations = $entity->validate();
$this->assertEqual(count($violations), 0, 'No violations found when in-range value passed.');
$entity->set($this->fieldStorage->getName(), 33);
$violations = $entity->validate();
$this->assertEqual(count($violations), 1, 'Violations found when using value outside the range.');
$this->assertEqual($violations[0]->getPropertyPath(), $this->fieldStorage->getName() . '.0.value');
$this->assertEqual($violations[0]->getMessage(), t('This value should be %limit or less.', [
'%limit' => 32,
]));
}
}
......@@ -146,3 +146,25 @@ function field_test_entity_extra_field_info_alter(&$info) {
// Remove all extra fields from the 'no_fields' content type;
unset($info['node']['no_fields']);
}
/**
* Implements hook_entity_bundle_field_info_alter().
*/
function field_test_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle) {
if (($field_name = \Drupal::state()->get('field_test_set_constraint', FALSE)) && $entity_type->id() == 'entity_test' && $bundle == 'entity_test' && !empty($fields[$field_name])) {
$fields[$field_name]->setPropertyConstraints('value', [
'Range' => [
'min' => 0,
'max' => 32,
],
]);
}
if (($field_name = \Drupal::state()->get('field_test_add_constraint', FALSE)) && $entity_type->id() == 'entity_test' && $bundle == 'entity_test' && !empty($fields[$field_name])) {
$fields[$field_name]->addPropertyConstraints('value', [
'Range' => [
'min' => 0,
'max' => 32,
],
]);
}
}
......@@ -133,39 +133,11 @@ function forum_uri($forum) {
}
/**
* Implements hook_node_validate().
*
* Checks in particular that the node is assigned only a "leaf" term in the
* forum taxonomy.
* Implements hook_entity_bundle_field_info_alter().
*/
function forum_node_validate(EntityInterface $node, $form, FormStateInterface $form_state) {
if (\Drupal::service('forum_manager')->checkNodeType($node)) {
// vocabulary is selected, not a "container" term.
if (!$node->taxonomy_forums->isEmpty()) {
// Extract the node's proper topic ID.
foreach ($node->taxonomy_forums as $delta => $item) {
// If no term was selected (e.g. when no terms exist yet), remove the
// item.
if (empty($item->target_id)) {
unset($node->taxonomy_forums[$delta]);
continue;
}
$term = $item->entity;
if (!$term) {
$form_state->setErrorByName('taxonomy_forums', t('Select a forum.'));
continue;
}
$used = \Drupal::entityQuery('taxonomy_term')
->condition('tid', $term->id())
->condition('vid', $term->bundle())
->range(0, 1)
->count()
->execute();
if ($used && !empty($term->forum_container->value)) {
$form_state->setErrorByName('taxonomy_forums', t('The item %forum is a forum container, not a forum. Select one of the forums below instead.', array('%forum' => $term->getName())));
}
}
}
function forum_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle) {
if ($entity_type->id() == 'node' && !empty($fields['taxonomy_forums'])) {
$fields['taxonomy_forums']->addConstraint('ForumLeaf', []);
}
}
......
<?php
/**
* @file
* Contains \Drupal\forum\Plugin\Validation\Constraint\ForumLeafConstraint.
*/
namespace Drupal\forum\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Checks that the node is assigned only a "leaf" term in the forum taxonomy.
*
* @Plugin(
* id = "ForumLeaf",
* label = @Translation("Forum leaf", context = "Validation"),
* )
*/
class ForumLeafConstraint extends Constraint {
public $selectForum = 'Select a forum.';
public $noLeafMessage = 'The item %forum is a forum container, not a forum. Select one of the forums below instead.';
}
<?php
/**
* @file
* Contains \Drupal\forum\Plugin\Validation\Constraint\ForumLeafConstraintValidator.
*/
namespace Drupal\forum\Plugin\Validation\Constraint;
use Drupal\Component\Utility\Unicode;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the ForumLeaf constraint.
*/
class ForumLeafConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($items, Constraint $constraint) {
if (!isset($items)) {
return;
}
$item = $items->first();
// Verify that a term has been selected.
if (!$item->entity) {
$this->context->addViolation($constraint->selectForum);
}
// The forum_container flag must not be set.
if (!empty($item->entity->forum_container->value)) {
$this->context->addViolation($constraint->noLeafMessage, array('%forum' => $item->entity->getName()));
}
}
}
<?php
/**
* @file
* Contains \Drupal\forum\Tests\ForumValidationTest.
*/
namespace Drupal\forum\Tests;
use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Term;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
/**
* Tests forum validation constraints.
*
* @group forum
*/
class ForumValidationTest extends EntityUnitTestBase {
/**
* Modules to install.
*
* @var array
*/
public static $modules = ['node', 'options', 'comment', 'taxonomy', 'forum'];
/**
* Tests the forum validation constraints.
*/
public function testValidation() {
// Add a forum.
$forum = Term::create([
'name' => 'forum 1',
'vid' => 'forums',
'forum_container' => 0,
]);
// Add a container.
$container = Term::create([
'name' => 'container 1',
'vid' => 'forums',
'forum_container' => 1,
]);
// Add a forum post.
$forum_post = Node::create([
'type' => 'forum',
'title' => 'Do these pants make my butt look big?',
]);
$violations = $forum_post->validate();
$this->assertEqual(count($violations), 1);
$this->assertEqual($violations[0]->getMessage(), 'This value should not be null.');
// Add the forum term.
$forum_post->set('taxonomy_forums', $forum);
$violations = $forum_post->validate();
$this->assertEqual(count($violations), 0);
// Try to use a container.
$forum_post->set('taxonomy_forums', $container);
$violations = $forum_post->validate();
$this->assertEqual(count($violations), 1);
$this->assertEqual($violations[0]->getMessage(), t('The item %forum is a forum container, not a forum. Select one of the forums below instead.', [
'%forum' => $container->label(),
]));
}
}
......@@ -68,6 +68,15 @@ protected function setUp() {
foreach (array_intersect(array('node', 'comment'), $class::$modules) as $module) {
$this->installEntitySchema($module);
}
if (in_array('forum', $class::$modules, TRUE)) {
// Forum module is particular about the order that dependencies are
// enabled in. The comment, node and taxonomy config and the
// taxonomy_term schema need to be installed before the forum config
// which in turn needs to be installed before field config.
$this->installConfig(['comment', 'node', 'taxonomy']);
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['forum']);
}
}
}
$class = get_parent_class($class);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment