HTML_QuickForm2_Rule

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

View base class in SVN

Issues for Milestone 4

removeRule()

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.

Rules on Containers

References:

Consider a Group of N text input fields which will be used for email input. Two types of validation are possible for this:

  • Check that at least M1 (and at most M2?) fields are not empty (and this is probably the only validation needed for multiple selects mentioned above);
  • That all the fields are either empty or contain valid emails.

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:

  • Nonempty / Required rule should be changed to be able to work on array of values (with special attention to InputFile elements, their values are arrays, too);
  • Hypothetic 'each' rule should be implemented, it will validate each Element within Container with the help of a (template) Rule provided in $options. Container will be considered valid if all elements are valid, if invalid the error message will be set on container rather than on separate elements.

Issues:

  • How to handle 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.

Rules configuration revisited

There are several problems with current implementation:

  • Rule configuration is stored in two different places. Calling getOptions() on a Rule instance gives us only a part of the whole picture.
  • Configuration is checked only on calling validate(), rather than on adding / configuring the Rule.

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:

  • Bad Rule configuration will be reported immediately;
  • 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”.

Issues to sort after Milestone 4

Client-side validation

Some ideas to consider

Rules as Array

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

Multiple error messages for an element (?)

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. :-(

Issues to sort before Milestone 2

Handling of "required" Rules

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:

  • Always add Required rules to the start of $rules array (?)
  • Disallow (by throwing exception) or_() method of Required rules (there is no problem in allowing and_())
  • Disallow (by throwing exception) chaining of Required rules to other rules via 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

Flyweight pattern

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

  1. now we add actual Rule objects to the elements via addRule()
  2. Rules need to have a “link” to the elements they validate

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

Rule configuration issues

I'm currently having problems with managing Rule configuration. We may have two sets of configuration parameters for a Rule:

  1. Parameters added when registering the Rule (e.g. a regular expression for a 'regexp' Rule)
  2. Additional parameters passed to Rule's constructor (e.g. maximum length parameter for a 'maxlength' 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

Porting of QuickForm version 3.x rules

Rules available:

Rules left to port:

  • “email” - should decide whether we want a better RFC822 compliance

Actually, we could have a more complete “rfc822” rule instead and rely on regex for common email validation.

  • “range” - we can leave only “minlength” and “maxlength”, since “rangelength” is easily represented by
    $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).

  • (file) “filename” - should probably work client-side, too. Come to think of it, do we need a separate Rule? Client-side it will be 100% equal to Regex and server-side we can just add a check for InputFile element to Regex, as is done in Nonempty. Added the check to Regex, it can now validate file uploads' names.

Set of useful regexes for web development

  • Integer (no leading 0 allowed)
-?([1-9]\d*|0)
  • Positive integer (including 0)
[1-9]\d*|0
  • Decimal
[-+]?[\d]*\.?[\d]+
  • Email (not RFC822 but common for website registration for example)
([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})
  • IP address (v4, basic)
(?:(?: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]?)
  • Username (range 6-16 chars)
(?!^[0-9]*$)(?!^[a-zA-Z_@]*$)(?!^[0-9_@]{1})^([a-zA-Z0-9_@]{6,16})$
  • URL (no query string, no fragment)
[A-Za-z][A-Za-z0-9]+:\/\/[[A-Za-z0-9]\-.]+(:[0-9]+)?\/[[A-Za-z0-9].\-~\/%]+
  • URL (more complete with querystring and fragment. Protocol is optional.)
(([\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})*)?
  • More to come ?
 
api/html_quickform2_rule.txt · Last modified: 2009/10/01 21:58 by sad_spirit