diff -u b/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php --- b/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -50,15 +50,20 @@ // Retrieve the constraints as objects. $constraint_set = $entity->getConstraintsObjects(); - // Loop all, if there's a matching form element, set #required. + + // Loop all constraints. foreach ($constraint_set as $field_name => $constraints) { + // Handle field constraints. if (isset($form[$field_name]) && !empty($constraints['field'])) { foreach ($constraints['field'] as $constraint) { + // @todo: find a better way to handle this, or use constraints passed + // by the field / typedData if (!isset($form[$field_name][0])) { $form[$field_name] += $constraint->convertToFormAPI(); } } } + // Handle typedData constraints. if (isset($form[$field_name]) && !empty($constraints[0])) { foreach ($constraints[0] as $constraint) { if (!isset($form[$field_name][0])) { diff -u b/core/lib/Drupal/Core/Validation/Constraint/Constraint.php b/core/lib/Drupal/Core/Validation/Constraint/Constraint.php --- b/core/lib/Drupal/Core/Validation/Constraint/Constraint.php +++ b/core/lib/Drupal/Core/Validation/Constraint/Constraint.php @@ -30,19 +30,42 @@ protected $message_arguments = array(); /** - * Settings of the constraint. + * Type of the constraint. * * @var string + * Either '', 'entity', 'field' or 'typedData' */ - protected $settings = array(); + protected $type = ''; /** - * Type of the constraint. + * The javascript rules. * - * @var string - * Either '', 'entity', 'field' or 'typedData' + * @var array + * Array containing all javascript rules to apply, keyed by .... */ - protected $type = ''; + protected $js_rules = array(); + + /** + * The (optional) custom javascript rule definitions. + * + * @var array + * Array of rules keyed by short name, pointing to a js file for each rule. + */ + protected $js_custom_rules = array(); + + /** + * The data definition. + * + * @var array + */ + protected $definition = array(); + + /** + * The properties. + * + * @var array + */ + protected $properties = array(); /** * Constructs a new constraint. @@ -50,21 +73,46 @@ * @param array $definition * Definition as specified in the plugin definition. * - * @param array $settings - * Settings passed by the entity, field or typed data. + * @param array $properties + * Properties passed by the entity, field or typed data. */ - public function __construct(array $definition, array $settings) { - $this->settings = $settings; + public function __construct(array $definition, array $properties) { + $this->definition = $definition; + if (isset($definition['message'])) { $this->message = $definition['message']; } if (isset($definition['type'])) { - $this->message = $definition['type']; + $this->type = $definition['type']; + } + + // Get properties from plugin definition. + if (isset($definition['properties'])) { + $this->properties = $definition['properties']; + } + + // Passed in properties override the definition properties. + if ($properties && is_array($properties)) { + $this->properties = $properties + $this->properties; } } - public function setSettings($settings) { - $this->settings = $settings; + /** + * Sets the properties to use. + * + * @param array $properties + */ + public function setProperties($properties) { + $this->properties = $properties + $this->properties; + } + + /** + * Gets the properties. + * + * @return array + */ + public function getProperties() { + return $this->properties; } /** @@ -112,2 +160,23 @@ + /** + * Gets the javascript rules. + * + * @return array + * Array containing all javascript rules to apply, keyed by .... + * This data will get passed to clientside validation. + */ + public function getJavascriptRules() { + return $this->$js_rules; + } + + /** + * Gets the custom javascript rules. + * + * @return array + * Array of rules keyed by short name, pointing to a js file for each rule. + * This data will get attached to the form. + */ + public function getJavascriptCustomRules() { + return $this->js_custom_rules; + } } diff -u b/core/lib/Drupal/Core/Validation/Constraint/ConstraintFactory.php b/core/lib/Drupal/Core/Validation/Constraint/ConstraintFactory.php --- b/core/lib/Drupal/Core/Validation/Constraint/ConstraintFactory.php +++ b/core/lib/Drupal/Core/Validation/Constraint/ConstraintFactory.php @@ -41,7 +41,7 @@ $definition = $this->discovery->getDefinition($plugin_id . '.field'); } // Checks to see if the target_object is an entity. - if (is_subclass_of($target) == 'Drupal\Core\Entity' || is_subclass_of($target, 'Drupal\Core\Entity\EntityInterface')) { + if (get_class($target) == 'Drupal\Core\Entity' || is_subclass_of($target, 'Drupal\Core\Entity\EntityInterface')) { $definition = $this->discovery->getDefinition($plugin_id . '.entity'); } } diff -u b/core/lib/Drupal/Core/Validation/Constraint/EntityTypeConstraint.php b/core/lib/Drupal/Core/Validation/Constraint/EntityTypeConstraint.php --- b/core/lib/Drupal/Core/Validation/Constraint/EntityTypeConstraint.php +++ b/core/lib/Drupal/Core/Validation/Constraint/EntityTypeConstraint.php @@ -15,8 +15,8 @@ * * @Plugin( * id = "entity type", - * label = @Translation("Entity type constraint"), - * message = @Translation("The value of %label is not a valid entity type."), + * label = @Translation("Entity type constraint", context = "Validation"), + * message = @Translation("The value of %label is not a valid entity type.", context = "Validation"), * type = "field" * ) */ @@ -29,6 +29,17 @@ - * TRUE if there's a value, FALSE if the value is empty. + * TRUE if value is a known entity type, FALSE if the value is empty. */ public function validate($value) { - return TRUE; + return array_key_exists($value, entity_get_info()); } -} + + /** + * Convert the constraint to form API structure. + * + * @return array + * Array usable by the form API. + */ + public function convertToFormAPI() { + return array( + '#choose' => array_keys(entity_get_info()), + ); + }} diff -u b/core/lib/Drupal/Core/Validation/Constraint/EqualFieldsConstraint.php b/core/lib/Drupal/Core/Validation/Constraint/EqualFieldsConstraint.php --- b/core/lib/Drupal/Core/Validation/Constraint/EqualFieldsConstraint.php +++ b/core/lib/Drupal/Core/Validation/Constraint/EqualFieldsConstraint.php @@ -15,8 +15,8 @@ * * @Plugin( * id = "equal.field", - * label = @Translation("Equal fields"), - * message = @Translation("The value of %field1 is different from %field2."), + * label = @Translation("Equal fields constraint", context = "Validation"), + * message = @Translation("The value of %field1 is different from %field2.", context = "Validation"), * type = "entity" * ) */ @@ -30,7 +30,22 @@ */ public function validate($value) { - $this->addMessageArguments('%field1', $value->get($this->settings[0])->getName()); - $this->addMessageArguments('%field2', $value->get($this->settings[1])->getName()); - return $value->get($this->settings[0])->getValue() == $value->get($this->settings[1])->getValue(); + $this->addMessageArguments('%field1', $value->get($this->properties['field1'])->getName()); + $this->addMessageArguments('%field2', $value->get($this->properties['field2'])->getName()); + return $value->get($this->properties['field1'])->getValue() == $value->get($this->properties['field2'])->getValue(); + } + + /** + * Convert the constraint to form API structure. + * + * @return array + * Array usable by the form API. + */ + public function convertToFormAPI() { + return array( + '#equal' => array( + $this->properties['field1'], + $this->properties['field2'], + ), + ); } } diff -u b/core/lib/Drupal/Core/Validation/Constraint/MinValueConstraint.php b/core/lib/Drupal/Core/Validation/Constraint/MinValueConstraint.php --- b/core/lib/Drupal/Core/Validation/Constraint/MinValueConstraint.php +++ b/core/lib/Drupal/Core/Validation/Constraint/MinValueConstraint.php @@ -15,9 +15,12 @@ * * @Plugin( * id = "min", - * label = @Translation("MinValue"), - * message = @Translation("The value of %label has to be greater than %min."), - * type = "typedData" + * label = @Translation("Min value constraint", context = "Validation"), + * message = @Translation("The value of %label has to be greater than %min.", context = "Validation"), + * type = "typedData", + * properties = { + * "min" = 0 + * } * ) */ class MinValueConstraint extends Constraint { @@ -29,8 +32,8 @@ * TRUE if the value is greater or equal, FALSE if the value is smaller. */ public function validate($value) { - $this->addMessageArguments('%min', current($this->settings)); - return $value >= current($this->settings); + $this->addMessageArguments('%min', $this->properties['min']); + return $value >= $this->properties['min']; } /** @@ -42,7 +45,7 @@ public function convertToFormAPI() { return array( '#type' => 'number', - '#min' => current($this->settings), + '#min' => $this->properties['min'], ); } } diff -u b/core/lib/Drupal/Core/Validation/Constraint/MinValueIntegerConstraint.php b/core/lib/Drupal/Core/Validation/Constraint/MinValueIntegerConstraint.php --- b/core/lib/Drupal/Core/Validation/Constraint/MinValueIntegerConstraint.php +++ b/core/lib/Drupal/Core/Validation/Constraint/MinValueIntegerConstraint.php @@ -15,9 +15,12 @@ * * @Plugin( * id = "min.integer", - * label = @Translation("MinValue"), - * message = @Translation("The value of %label has to be greater than %min."), - * type = "typedData" + * label = @Translation("Min integer constraint", context = "Validation"), + * message = @Translation("The value of %label has to be greater than %min.", context = "Validation"), + * type = "typedData", + * properties = { + * "min" = 0 + * } * ) */ class MinValueIntegerConstraint extends MinValueConstraint { @@ -30,6 +33,19 @@ */ public function validate($value) { - $this->addMessageArguments('%min', current($this->settings)); - return $value >= current($this->settings); + $this->addMessageArguments('%min', $this->properties['min']); + return $value >= $this->properties['min']; + } + + /** + * Convert the constraint to form API structure. + * + * @return array + * Array usable by the form API. + */ + public function convertToFormAPI() { + return array( + '#type' => 'number', + '#min' => $this->properties['min'], + ); } } diff -u b/core/lib/Drupal/Core/Validation/Constraint/NotNullConstraint.php b/core/lib/Drupal/Core/Validation/Constraint/NotNullConstraint.php --- b/core/lib/Drupal/Core/Validation/Constraint/NotNullConstraint.php +++ b/core/lib/Drupal/Core/Validation/Constraint/NotNullConstraint.php @@ -15,8 +15,8 @@ * * @Plugin( * id = "notnull", - * label = @Translation("NotNull"), - * message = @Translation("The value of %label can not be null."), + * label = @Translation("Not null constraint", context = "Validation"), + * message = @Translation("The value of %label can not be null.", context = "Validation"), * type = "typedData" * ) */ @@ -33,2 +33,13 @@ } + + /** + * Convert the constraint to form API structure. + * + * @return array + * Array usable by the form API. + */ + public function convertToFormAPI() { + // @todo: implementqtion needed. + return array('#notnull' => TRUE); + } } diff -u b/core/lib/Drupal/Core/Validation/Constraint/RequiredConstraint.php b/core/lib/Drupal/Core/Validation/Constraint/RequiredConstraint.php --- b/core/lib/Drupal/Core/Validation/Constraint/RequiredConstraint.php +++ b/core/lib/Drupal/Core/Validation/Constraint/RequiredConstraint.php @@ -15,8 +15,8 @@ * * @Plugin( * id = "required", - * label = @Translation("Required"), - * message = @Translation("The value of %label is required."), + * label = @Translation("Required constraint", context = "Validation"), + * message = @Translation("The value of %label is required.", context = "Validation"), * type = "typedData" * ) */ diff -u b/core/lib/Drupal/Core/Validation/Constraint/RequiredFieldConstraint.php b/core/lib/Drupal/Core/Validation/Constraint/RequiredFieldConstraint.php --- b/core/lib/Drupal/Core/Validation/Constraint/RequiredFieldConstraint.php +++ b/core/lib/Drupal/Core/Validation/Constraint/RequiredFieldConstraint.php @@ -15,8 +15,8 @@ * * @Plugin( * id = "required.field", - * label = @Translation("Required"), - * message = @Translation("The value of %label is required."), + * label = @Translation("Required field constraint", context = "Validation"), + * message = @Translation("The value of %label is required.", context = "Validation"), * type = "field" * ) */ diff -u b/core/modules/system/lib/Drupal/system/Tests/Validation/ValidationConstraintTest.php b/core/modules/system/lib/Drupal/system/Tests/Validation/ValidationConstraintTest.php --- b/core/modules/system/lib/Drupal/system/Tests/Validation/ValidationConstraintTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Validation/ValidationConstraintTest.php @@ -50,2 +50,22 @@ } + + /** + * Tests MinValueConstraint + */ + public function testMinValueConstraint() { + // Test using the default value for min. + $constraint = new Constraint\MinValueConstraint(array('properties' => array('min' => 0)), array()); + $this->assertTrue($constraint->validate(5)); + $this->assertTrue($constraint->validate(0)); + $this->assertTrue($constraint->validate('')); + $this->assertFalse($constraint->validate(-2)); + + // Test using a custom value for min. + $constraint = new Constraint\MinValueConstraint(array('properties' => array('min' => 0)), array('min' => 5)); + $this->assertTrue($constraint->validate(5)); + $this->assertFalse($constraint->validate(4)); + $this->assertFalse($constraint->validate(0)); + $this->assertFalse($constraint->validate(-10)); + $this->assertFalse($constraint->validate('')); + } } only in patch2: unchanged: --- /dev/null +++ b/core/lib/Drupal/Core/Validation/README.txt @@ -0,0 +1,123 @@ +Rough technical overview of how it is implemented right now + +Constraints can be defined on 3 levels: +entity + - field [0..n] + - typedData [1..n] + - property [1..n] + - typedData [1..n] + +FormController->build() calls entity->getConstraintsObjects() + - gets a list of constraints keyed by field/property name + +Each constraint has a method convertToFormAPI() which returns an array containing keyed data compatibnle with the form API +ex: +return array( + '#type' => 'number', + '#min' => current($this->settings), +); + +FormController->validate() calls $entity->validate + this loops over all fields and typedData, calling validate() on each + validate() will get the constraints and validates the value/field + all violations are collected and returned to the parent + FormController uses set_form_error for each violation + +Constraints uses the Plugin system for it's definition +ex: + * @Plugin( + * id = "min", + * label = @Translation("MinValue"), + * message = @Translation("The value of %label has to be greater than %min.") + +The message is added to the plugin system so it gets detected by l.d.o. +The Constraint base class has a method addMessageArguments so it can prefill some placeholders +ex: +$this->addMessageArguments('%min', current($this->settings)); + +The FormController calls uses getMessage, getMessageArguments, getObjectLabel to build the (translated) message. +ex: +form_set_error('error', + t($violation->getMessage(), + $violation->getMessageArguments() + array('%label' => $violation->getObjectLabel())) +); + +Constraints: +Indented to show parent - child relationship. +The factory used to choose the most appropriate Constraint accepts an object as parameter, this is either a TypedData, a Field or an Entity. The factory looks at the type of the object ('integer, 'field', ...) and uses the most specific class it can find. + +Constraint.php + EntityTypeConstraint.php --> array('#choose' => array_keys(entity_get_info())) + EqualFieldsConstraint.php --> array('#equal' => array($this->settings[0], $this->settings[1])) + MinValueConstraint.php --> array('#type' => 'number', '#min' => current($this->settings)) + MinValueIntegerConstraint.php --> array('#type' => 'number', '#min' => current($this->settings)) + NotNullConstraint.php --> array('#notnull' => TRUE) + RequiredConstraint.php --> array('#required' => TRUE) + RequiredFieldConstraint.php --> inherited + +Connect the dots: Entity to FormAPI +entity -> $form['#constraints'] + - field -> $form['field_name']['#constraints'] + - typedata -> $form['field_name'][$delta]['#constraints'] + +Form key always has to match the field_name, or use #bound_to +FormController adds data (Constraint) to the form elements, in phase 1 it also adds the FormAPI structure so it can run side by side. +AfterBuild converts Constraints to FormAPI structure, in phase 1 some FormAPI elements are supported as well. + +@TODO: getConstraints on each field has to return full objects +@TODO: add afterBuild method to FormController to handle the conversion + + + +Follow up issues: +Add support for validation on forms not tight to an entity. + +Add clientside validation support, see [#1797438] + add it in an afterBuild phase, so form_alter still works. + use clientside_validation, but allow rule definition on the constraint. + +Add constraint selection on instance_field_settings_form + which constraints do we need to show: structured tree containing field_type - widget_type + some constraint cannot be removed (like IntegerConstraint on an Integer text field) + +List all field types and subtypes (like integer select list) and + provide all constraints + organize the constraints in a tree (inheritance) structure + +How to handle conflicts (like max < min)? + +Do we need weight/importance/priority on Constraints? + +Decide if we still support old '#required' + example before + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Name'), + '#default_value' => 'default', + '#size' => 60, + '#weight' => -10, + '#maxlength' => 128, + '#required' => TRUE, + ); + example after + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Name'), + '#default_value' => 'default', + '#size' => 60, + '#weight' => -10, + '#constraints' => array( + new MaxLengthConstraint(array( + 'message' => 'or use default one from constraint', + 'maxlength' => 128, + )), + new RequiredFieldConstraint(array( + 'message' => 'or use default one from constraint', + )), + ), + ); + +Validation phase has to pickup the custom messages + + +