Usage example, lifted from builtin-rules.php example file
// this behaves exactly as it reads: either "password" and "password repeat" // are empty or they should be equal, password should be no less than 6 chars // and old password should be given $newPassword->addRule('empty') ->and_($repPassword->createRule('empty')) ->or_($repPassword->createRule('eq', 'The passwords do not match', $newPassword)) ->and_($newPassword->createRule('minlength', 'The password is too short', 6)) ->and_($oldPassword->createRule('nonempty', 'Supply old password if you want to change it'));
Currently the following code
$rule = $elementOne->addRule('foo', 'an error'); $elementTwo->addRule($rule);
results in $rule validating $elementTwo while being attached to both $elementOne and $elementTwo and thus called twice.
HTML_QuickForm2_Node::removeRule() should be implemented. It will be called from HTML_QuickForm2_Rule::setOwner() to prevent such issues.
References:
Consider a Group of N text input fields which will be used for email input. Two types of validation are possible for this:
Note that QuickForm 3.x mixed these in its addGroupRule(), so with
$form->addGroup(array( $form->createElement('text', '0'), $form->createElement('text', '1') ), 'emails', 'At least one email:', '<br />', true); $form->addGroupRule('emails', 'please input at least one email', 'required', null, 1); $form->addGroupRule('emails', 'emails should be valid', 'email', null, 1);
'emails' group will be considered valid if one of the fields contains 'user@example.com' and the other one 'some random junk'. Of course, $homwany parameter defaulted to number of grouped elements, so this WTF only appeared if providing it manually.
It may make sense to separate these in QuickForm2, doing something along the lines of
$group = $form->addElement('group', 'emails'); $group->addElement('text', '0'); $group->addElement('text', '1'); $group->addRule('required', 'please input at least one email', array('min' => 1)); $group->addRule('each', 'emails should be valid', $group->createRule('email'));
Changes:
Issues:
and_() / or_() chaining with template Rule?One idea for the above: 'each' Rule will clone the template Rule it uses for validating, and we shall implement HTML_QuickForm2_Rule::__clone() that'll just remove all chained rules on cloning.
There are several problems with current implementation:
getOptions() on a Rule instance gives us only a part of the whole picture.I think it'll make sense to change the Rule API in the following way:
abstract class HTML_QuickForm2_Rule { public function __construct(HTML_QuickForm2_Node $owner, $message = '', $options = null, $globalOptions = null) { $this->setOwner($owner); $this->setMessage($message); $this->setOptions($this->mergeOptions($options, $globalOptions)); } protected function mergeOptions($options, $globalOptions) { // will handle merging local (provided to a Rule instance) and global // (registered with QF2_Factory) Rule configuration with proper precedence } public function setOptions($options) { if (isBad($options)) { throw new HTML_QuickForm2_InvalidArgumentException( "Rule options are BAD" ); } $this->options = $options; } }
Benefits:
getOptions() will return full Rule configuration, rather than part of it.
While we are busy breaking BC, it may make sense to rename setOptions() / getOptions() to setConfig() / getConfig(), since configuration data is mostly mandatory for Rules, not “optional”.
Client-side validation
It would be nice to also accept an array, to construct the whole chain:
$rules = array(); $r1 = $element->createRule('Correct input', 'rulename1'); $r2 = $element->createRule('Correct input', 'rulename2'); $r3 = $element->createRule('Correct input', 'rulename3'); $r4 = $element->createRule('Correct input', 'rulename4'); $r5 = $element->createRule('Correct input', 'rulename5'); // r1 OR r2 OR (r3 AND r4) $rules = array($r1, 'or', $r2, 'or', array($r3, 'and', array($r4, 'or', $r5))); $form->addRule($rules);
Doesn't look more readable to me than
$form->addRule($r1)->or_($r2)->or_($r3->and_($r4->or_($r5)));
(yes, parentheses here will work as expected) — Alexey Borzov 2007-07-26 17:48
Currently the validation behaviour is as follows: Rule chains (Rules added via and_() and or_()) are validated until the result can be divined (As in PHP itself, see http://www.php.net/manual/en/migration.booleval.php). Rules added via addRule() are evaluated until an error message appears. Only one error message is possible for an element and appearance of this message stops further validation.
It may be interesting to allow several error messages for one element, but I couldn't come up with a good API for this, since we need to somehow differentiate between errors that should stop all further validation and those that should allow it to continue.
Required rules are a bit different from others since they change element rendering: * denotes required field. Therefore it is difficult to make them “conditional”.
One possible approach is to handle them differently from all other rules, using setRequired() or similar.
Another approach is to use the same addRule() method but:
$rules array (?)or_() method of Required rules (there is no problem in allowing and_())and_() and or_()I tend to like the second approach better as it makes porting from previous version a bit easier and keeps API simpler.
Another solution is to use setRequired() only to visually mark the element as being required (with a red * for example) and have a NotEmpty rule (or whatever) added by the user with addRule(). Then, an element could be allowed to be empty if another element is not, etc. but still be marked as being required. — Bertrand Mansion 2007-05-16 09:10
Commited the implementation outlined here, except the idea on adding Required rules only to the front of $rules array. Also added isRequired() method to Node which naturally checks whether any Required rules were added to the element. Looks good to me, though other may disagree.
— Alexey Borzov 2007-07-05 21:20
In current (3.2.7) QuickForm, there are 13 registered rules (17 if one counts the ones registered in file.php) and only 6 Rule subclasses. RuleRegistry class stores only one instance of a Rule of each class. Differentiation of Rule behaviours is done via Rule “names”.
It is not possible to directly port this to QuickForm2 since
addRule()
On the other hand, we'll probably want to keep [number of registered rules] > [number of Rule subclasses]. An idea I have currently is to keep some default configuration info alongside Rule class name and file name in Factory (when Rule registering and creating is done there). I do not want to differentiate behaviour based on Rule name since we have to keep both 'name' ⇒ 'classname' association in _HTML_QuickForm_registered_rules array and 'name' ⇒ behaviour association in actual Rule class (see HTML_QuickForm_Rule_Regex for an example).
I'm currently having problems with managing Rule configuration. We may have two sets of configuration parameters for a Rule:
In QuickForm 3.x the first kind of parameters were kept in Rule object itself (as we had only one instance of each Rule class) and the second kind were kept in HTML_QuickForm::$_rules and were passed to the Rule when performing actual validation.
In QuickForm2 we need to supply both kinds of parameters to the Rule object when instantiating it. Most of the time this instantiation will be performed by Factory, but we shouldn't make the parameters list too big and convoluted in respect to those users who will instantiate the objects “manually”.
Now the thing works as follows: “global” parameters for a rule type are added to the Factory when registering a rule. Factory provides a special $registeredType parameter to a Rule constructor, which allows getting this configuration parameters via getRuleConfig() method. “Local” parameters are supplied when instantiating the Rule object via $options parameter. — Alexey Borzov 2007-07-05 21:20
Rules available:
Rules left to port:
Actually, we could have a more complete “rfc822” rule instead and rely on regex for common email validation.
$field->addRule('minlength', 'Should be 5 to 20 characters', 5) ->and_($field->createRule('maxlength', '', 20));
I agree. After implementing the Length rule, it turned out that it won't be spectacularly simpler if it couldn't do the equivalent of 'rangelength' validation. So it is available (as 'length' rule).
-?([1-9]\d*|0)
[1-9]\d*|0
[-+]?[\d]*\.?[\d]+
([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})
(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
(?!^[0-9]*$)(?!^[a-zA-Z_@]*$)(?!^[0-9_@]{1})^([a-zA-Z0-9_@]{6,16})$
[A-Za-z][A-Za-z0-9]+:\/\/[[A-Za-z0-9]\-.]+(:[0-9]+)?\/[[A-Za-z0-9].\-~\/%]+
(([\w]+:)?//)?(([\d\w]|%[a-fA-f\d]{2,2})+(:([\d\w]|%[a-fA-f\d]{2,2})+)?@)?([\d\w][-\d\w]{0,253}[\d\w]\.)+[\w]{2,4}(:[\d]+)?(/([-+_~.\d\w]|%[a-fA-f\d]{2,2})*)*(\?(&?([-+_~.\d\w]|%[a-fA-f\d]{2,2})=?)*)?(#([-+_~.\d\w]|%[a-fA-f\d]{2,2})*)?