SitePoint Sponsor

User Tag List

Results 1 to 12 of 12
  1. #1
    SitePoint Zealot
    Join Date
    Oct 2003
    Location
    Denmark
    Posts
    129
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    Validator design

    Following up on http://www.sitepoint.com/forums/showthread.php?t=445728 and after reading Jasons book, I have tried to create the following validator:

    http://svn.intraface.dk/intrafacepub.../Validator.php

    Now I need to know, how I can improve the error messages when using my AndRule and OrRule?

    Other than that, would this be a sound approach to the validation? Or could you wise men improve upon it?

    And say I want to use it with the active record pattern, when saving, would it be clever to do:

    PHP Code:
    class ActiveRecord {
        function 
    save($dataValidator $validator) {
            if (!
    $validator->isSatisfiedBy($data)) return false;
        }
    }

    $rec = new ActiveRecord();
    if (!
    $rec->save($data$validator)) {
        
    print_r($validator->getErrors());


  2. #2
    SitePoint Evangelist
    Join Date
    Aug 2004
    Posts
    428
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    Jasons book?

    first off what is "Jasons book"

    lets talk about the api before we get into specific implementation...
    heres how as a developer i would like to make my forms.


    PHP Code:
    $form = new Form('uniqueformname''ControlRenderThemePrefix' ); 
    //activecontrols or normal controls
    $form->addControl( new Literal('idliteralcontrol''<div>html to insert</div>'));
    $form->addControl( new TextBox('idtextbox''text',TextBox::multiline) );
    $form->addControl( new Button('idbutton'Button::submit'onclickfunctiondelegate') );
    $form->addRule( new RequiredFieldValidator'id''idtextbox''cannot be empty''displaynone'));  //don't add the validation inline
    $form->addValidationSummary(new ValidationSummary('with javascript popup'));  //show a summary box containing all errors on the form. 
    this is along the lines of how .net does it. I added my controlrenderthemeprefix so that i can move my rendering logic (html logic) outside my controls. knowing the prefix helps instantiate the right visitors that know how to render each control in my form. This way i can create a theme of visitors for ajax controls and normal controls...

  3. #3
    SitePoint Zealot
    Join Date
    Oct 2003
    Location
    Denmark
    Posts
    129
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Jasons book = Jason E. Sweat aka sweatje: Guide to PHP Design Patterns.

    One should be able to use a validator, not only with forms, however it should also play nicely with forms off course.

    I would think the following would be quite clever, if I should use it with forms.

    PHP Code:
    $form = new Form('uniqueformname');
    $form->addControl( new TextBox('idtextbox''text',TextBox::multiline) );
    $form->addControl( new Button('idbutton'Button::submit'onclickfunctiondelegate') );
    $form->addValidator($validator);  //don't add the validation inline
    $form->display($renderer); 
    If I should use it same validator with a save method:

    PHP Code:
    function save($data$validator) {
        if (
    $validator->isSatisfiedBy($data)) do_save();
        else die(
    'something went horribly wrong');
    }

    save(array(), $validator); 

    I think I would prefer to pass the renderer to the display() method, as it seems that the classes would be more decoupled then. What I want to achieve is have the validation decoupled from the form, so it simply does one job.

  4. #4
    SitePoint Evangelist
    Join Date
    Aug 2004
    Posts
    428
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    nice catch

    $form->addValidator($validator);
    $form->display($renderer);
    i like


    my previous approach required all controls to have an associated presentation logic visitor.
    TextBox has VisitorTextBox or ActiveVisitorTextBox

    that is why in my form object i specify the prefix of the visitors to instantiate...
    default for a control is to accept the form visitor specified... or you can do this:
    $form->addControl( new TextBox('idtextbox', 'text',TextBox::multiline, 'WowVisitor') );
    //this specific control will use WowVisitor instead of the form specified visitor prefix


    in your scenario you have a 1 renderer object which knows how to specifically render this 1 form and all the controls. This could alleviate the literal control crap and the order dependency of adding and displaying controls but could also become very bloated.


    good to see someone elses idea on the subject.


    pragmatically speaking you won't use validation objects outside forms.

  5. #5
    SitePoint Zealot
    Join Date
    Oct 2003
    Location
    Denmark
    Posts
    129
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I disagree that you would only need validation in forms. Suppose I have some data which can either be submitted by a form or by a webservice sending by xmlrpc. I have to validate the input i both situations, and I want to use the same validator. Suppose I just want to make sure, that data is validated just before it is saved also, I also want to use the same validator.

    I see your point about the renderer. I would be nice to have other peoples take on that. Suppose you would like to make a generic form class which could be used both for html, but also for phpgtk or the commandline.

  6. #6
    SitePoint Evangelist
    Join Date
    Aug 2004
    Posts
    428
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    lets summarize for everyone

    //try this scenario:
    summary of errors with javascript popup
    textbox required
    button
    summary of errors


    //.net version with my presentation abstraction
    PHP Code:

    $form 
    = new Form('uniqueformname''ControlRenderThemePrefix' ); 
    $form->addValidationSummary(new ValidationSummary('with javascript popup'));
    $form->addControl( new Literal('idliteralcontrol''<div>html to insert</div>'));
    $form->addControl( new TextBox('idtextbox''text',TextBox::multiline'WowVisitor') );
    $form->addControl( new Button('idbutton'Button::submit'onclickfunctiondelegate') );
    $form->addRule( new RequiredFieldValidator'id''idtextbox''cannot be empty''displaynone'));
    $form->addValidationSummary(new ValidationSummary('without javascript popup'));
    $form->display(); 






    my implementation of lsolesen and my presentation ideas:
    PHP Code:
        class MyRender
        
    {
            private 
    $htmlqueue = array();


            public 
    visitor(TextBox t)
            {
                
    $html="";
                if(
    $t->ID == 'idtextbox')
                {
                    
    $visitor = new WowTextBox();
                    
    $html $visitor->render(t);
                }
                else
                {
                    
    $visitor = new ActiveTextBox();
                    
    $html $visitor->render(t);
                }
                
    $this->htmlqueue[$t->ID] = $html;
            }

            public 
    visitor(Button b)
            {
                
    $visitor = new ActiveButton();
                
    $this->htmlqueue[$b->ID] = $visitor->render(b);
            }

            public 
    visitor(ValidationSummary s)
            {
                
    $visitor = new ActiveValidationSummary();
                
    $this->htmlqueue[$s->ID] = $visitor->render(s);
            }

            public 
    render()
            {
                
    $html "My First Form";
                
    $html .= $this->htmlqueue['idsummary2'];
                
    $html .= $this->htmlqueue['idtextbox'];
                
    $html .= $this->htmlqueue['idbutton'];
                
    $html .= $this->htmlqueue['idsummary'];
                return 
    $html;
            }
        }

    $validator = new Validation();
    $validator->addRule(new RequiredFieldValidator('idtextbox''cannot be empty''displaynone'));
    $validator->addValidationSummary(new ValidationSummary('idsummary''without javascript popup'));
    $validator->addValidationSummary(new ValidationSummary('idsummary2''with javascript popup'));



    $render = new MyRender();



    $form = new Form('uniqueformname');
    $form->addControl( new Button('idbutton'Button::submit'onclickfunctiondelegate') );
    $form->addControl( new TextBox('idtextbox''text',TextBox::multiline) );
    $form->addValidator($validator); 
    $form->display($renderer); 


    the above proves the order doesn't matter in the more abstracted version... you have to syncronize the ids...but that is about it.

  7. #7
    eschew sesquipedalians silver trophy sweatje's Avatar
    Join Date
    Jun 2003
    Location
    Iowa, USA
    Posts
    3,749
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    In one of my earliest attempts at the specification pattern I included a "messageCollector" object along with the item being validated, and this was passed all down the specificationChain. I actually implemented this as an observable object, so I would attach whatever objects I wanted notified before I performed the validation. Each individual specification was responsible for not only indicating a pass or a failure, but also to know how to appropriately format an error message to pass to the messageCollector object as well.
    Jason Sweat ZCE - jsweat_php@yahoo.com
    Book: PHP Patterns
    Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
    Detestable (adjective): software that isn't testable.

  8. #8
    SitePoint Zealot
    Join Date
    Oct 2003
    Location
    Denmark
    Posts
    129
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    sweatje: What about your current attempts?

  9. #9
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Here's another take: conjunctions (and) disjunctions (or) and negation. Different types can be nested within each other to create complex predicates. isValid will accept any number of args - sometimes I pass stuff in here or sometimes not (ie the rule constructor).

    I'm actually kind of stuck on errors. It sounds reasonable that the object which holds the error state should define the error but I don't think that's always the case. Sometimes it's the client object which has the context in which individual failed conditions take on meaning. Returning an error string introduces a view responsibility (the choice of language) to a domain object and so perhaps rules should always return error codes? That adds extra complexity which you might not need. Sometimes you don't need an error message at all just the boolean return value - and you'll probably have classes you want to use in both silent and noisy cases.

    So I don't have any answers for the ultimate validation system: just comparing notes.

    PHP Code:
    // some common tests for Conjunction & Disjunction
    class SpecificationTestCase extends AperiUnitTest {

        function &
    _InstanceOfTestedClass() {
            
    // abstract
        
    }
        function 
    testReturnBooleanTrueWhenNoRulesHaveBeenAdded() {
            
    $spec =& $this->_InstanceOfTestedClass();
            
    $this->assertIdentical($spec->isValid(), true);
        }
        function 
    testCanAddRule() {
            
    $spec =& $this->_InstanceOfTestedClass();
            
    $rule =& new MockRule;
            
    $spec->add($rule);
            
    $rule->expectOnce('isValid', array());
            
    $rule->setReturnValue('isValid'false);
            
    $this->assertIdentical($spec->isValid(), false);
        }
        function 
    testCanAddSpecification() {
            
    $topmost_spec =& $this->_InstanceOfTestedClass();
            
    $nested_spec =& $this->_InstanceOfTestedClass();
            
    $rule =& new MockRule;
            
            
    $nested_spec->add($rule);
            
    $topmost_spec->add($nested_spec);
            
    $rule->expectOnce('isValid', array());
            
    $rule->setReturnValue('isValid'false);
            
    $this->assertIdentical($topmost_spec->isValid(), false);
        }
        function 
    testIsValidCanAcceptVariableNumberOfArgs() {
            
    $spec =& $this->_InstanceOfTestedClass();
            
    $rule =& new MockRule;
            
    $spec->add($rule);
            
    $rule->expectCallCount('isValid'2);
            
    $rule->expectArgumentsAt(0'isValid', array());
            
    $rule->expectArgumentsAt(1'isValid', array('foo''bar'));
            
    $spec->isValid();
            
    $spec->isValid('foo''bar');
            
    $this->assertNoErrors();
        }
    }
    class 
    TestConjunction extends SpecificationTestCase {

        function &
    _InstanceOfTestedClass() {
            
    $spec =& new Conjunction;
            return 
    $spec;
        }
        function 
    testIsValidReturnsBooleanTrueWhenAllRulesAreValid() {
            
    $spec =& $this->_InstanceOfTestedClass();
            
    $rule_0 =& new MockRule;
            
    $rule_1 =& new MockRule;
            
    $spec->add($rule_0);
            
    $spec->add($rule_1);

            
    $rule_0->setReturnValue('isValid'true);
            
    $rule_1->setReturnValue('isValid'true);
            
    $this->assertIdentical($spec->isValid(), true);
        }
        function 
    testIsValidReturnsBooleanFalseWhenAnySingleRuleIsInvalid() {
            
    $spec =& $this->_InstanceOfTestedClass();
            
    $rule_0 =& new MockRule;
            
    $rule_1 =& new MockRule;
            
    $spec->add($rule_0);
            
    $spec->add($rule_1);

            
    $rule_0->setReturnValue('isValid'true);
            
    $rule_1->setReturnValue('isValid'false);
            
    $this->assertIdentical($spec->isValid(), false);
        }
    }
    class 
    TestDisjunction extends SpecificationTestCase {

        function &
    _InstanceOfTestedClass() {
            
    $spec =& new Disjunction;
            return 
    $spec;
        }
        function 
    testIsValidReturnsBooleanTrueWhenAnySingleRuleIsValid() {
            
    $spec =& $this->_InstanceOfTestedClass();
            
    $rule_0 =& new MockRule;
            
    $rule_1 =& new MockRule;
            
    $spec->add($rule_0);
            
    $spec->add($rule_1);

            
    $rule_0->setReturnValue('isValid'false);
            
    $rule_1->setReturnValue('isValid'true);
            
    $this->assertIdentical($spec->isValid(), true);
        }
        function 
    testIsValidReturnsBooleanFalseWhenAllRulesAreInvalid() {
            
    $spec =& $this->_InstanceOfTestedClass();
            
    $rule_0 =& new MockRule;
            
    $rule_1 =& new MockRule;
            
    $spec->add($rule_0);
            
    $spec->add($rule_1);

            
    $rule_0->setReturnValue('isValid'false);
            
    $rule_1->setReturnValue('isValid'false);
            
    $this->assertIdentical($spec->isValid(), false);
        }
    }
    class 
    TestNegatedSpecification extends AperiUnitTest {

        function 
    setUp() {
            
    $this->spec =& new MockSpecification;
            
    $this->negated =& new NegatedSpecification($this->spec);
        }
        function 
    testReturnFalseWhenSpecificationIsTrue() {
            
    $this->spec->expectOnce('isValid', array());
            
    $this->spec->setReturnValue('isValid'true);
            
    $this->assertIdentical($this->negated->isValid(), false);
        }
        function 
    testReturnTrueWhenSpecificationIsFalse() {
            
    $this->spec->expectOnce('isValid', array());
            
    $this->spec->setReturnValue('isValid'false);
            
    $this->assertIdentical($this->negated->isValid(), true);
        }
        function 
    testIsValidSupportsVariableNumberOfParameters() {
            
    $this->spec->expectOnce('isValid', array('foo''bar'));
            
    $this->negated->isValid('foo''bar');
        }
        function 
    testCanAddRules() {
            
    $rule =& new MockRule;
            
    $this->spec->expectOnce('add', array($rule));
            
    $this->negated->add($rule);
        }
    }
    class 
    Specification {   // implements Rule

        
    var $_rules = array();
        var 
    $true = array();
        var 
    $false = array();

        function 
    add(&$rule) {
            
    $this->_rules[] =& $rule;
        }
        function 
    isValid() {
            
    $args func_get_args();
            
    $this->_evaluate($args);
            return 
    $this->_xjunction();
        }
        function 
    _evaluate($args) {
            
    $this->_reset();
            foreach(
    array_keys($this->_rules) as $key) {
                if(
    call_user_func_array(
                        array(&
    $this->_rules[$key], 'isValid'), 
                        
    $args)) {
                    
    $this->true[] =& $this->_rules[$key];
                } else {
                    
    $this->false[] =& $this->_rules[$key];                
                }
            }
        }
        function 
    _reset() {
            
    $this->true = array();
            
    $this->false = array();
        }
        function &
    getTrue() {
            return 
    $this->true;
        }
        function &
    getFalse() {
            return 
    $this->false;
        }
        function 
    _xjunction() {
            
    // abstract
        
    }
    }
    class 
    Conjunction extends Specification // implements Rule

        
    function _xjunction() {
            return 
    count($this->false) == 0;
        }
    }
    class 
    Disjunction extends Specification // implements Rule

        
    function _xjunction() {
            return 
    count($this->true) > 
                
    or count($this->_rules) == 0;
        }
    }
    /*
    */
    class NegatedSpecification {
        function 
    NegatedSpecification(&$spec) {
            
    $this->_spec =& $spec;
        }
        function 
    isValid() {
            
    $args func_get_args();
            return !
    call_user_func_array(
                array(&
    $this->_spec'isValid'), 
                
    $args);
        }
        function 
    add(&$rule) {
            
    $this->_spec->add($rule);
        }


  10. #10
    eschew sesquipedalians silver trophy sweatje's Avatar
    Join Date
    Jun 2003
    Location
    Iowa, USA
    Posts
    3,749
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by lsolesen View Post
    sweatje: What about your current attempts?
    I looked back over implementations where I used the specification pattern. They broke down into three version.

    1) pass the message collecter along with the subject version
    2) change the specification itself into an observable object
    3) do not rely on the specification itself, but only on the pass/fail and handle at a higher level

    It seems recently I use option 2 where the detail of individual members of the policy matter, and option 3 where only the end result of the policy matters.

    I suppose alternative communications from the individual specifications might include exceptions (abusive in my mind), errors, and appending to some kind of a global message queue (global meaning static class, function or an actual global variable).

    HTH
    Jason Sweat ZCE - jsweat_php@yahoo.com
    Book: PHP Patterns
    Good Stuff: SimpleTest PHPUnit FireFox ADOdb YUI
    Detestable (adjective): software that isn't testable.

  11. #11
    SitePoint Evangelist
    Join Date
    Aug 2004
    Posts
    428
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    alternative view

    IMHO using the specification pattern for validation seems wrong. From the design pattern its goal is to return a list based on specific rules.
    getRules(typeof(RequiredFieldValidator));
    getRules('idtextbox');
    ....
    getErrors(typeof(RequiredFieldValidator));
    getErrors('idtextbox');
    ....
    to use the spec pattern a lot more queries would be needed before you can justify the effort to move to this pattern.


    In essence your than using errors.Length == 0 to check if the form is valid. A lot more than simply adding a RequiredFieldValidator object and checking if that rule is valid or the validation summary is valid directly.


    observer object? curious to know what listeners you have.?. I only perform work only if it IsValid == true else I reissue the form with previous submitted values and a validation summary showing the errors. Are you emailing yourself if a specific error is caught - mind sharing the ideas behind your observer here? [falls into your category if policy matters i would think]



    [QUOTE]
    I use option 2 where the detail of individual members of the policy matter
    ..........
    I suppose alternative communications from the individual specifications might include exceptions (abusive in my mind)
    [QUOTE]


    exactly if policy matters i opt for exceptions: no need for a global message queue since the first policy failed should stop any further processing.

    Good example of this authentication:

    PHP Code:
                try
                {
                    
    Authenticate auth = new Authenticate(this.usernamethis.passwordthis.cbxRememberMe);
                    
    auth.LoginEvent += new Authenticate.LoginHandler(auth_LoginEvent);

                    
    auth.run();
                }
                catch (
    AccountDetailsException exp)
                {
                    
    this.lblErrorMessage.Text = @"
                        <br />
                        Your account requires updating before using the new site.  
                        An email has been sent to our web administrator to fix your account.  
                        We will be contacting you in the next 24 hours.

                        <br />
                        <br />
                        Please bare with us as we migrate to a more functional website.
                    "
    ;
                    
    this.notifyAdministrator
                    
    (
                        
    String.Format("message seen by user:{0}\n\nusername:{1}\npassword:{2}\n\n\nexception type:{3}",
                        
    lblErrorMessage.Textthis.username.Textthis.password.Textexp.ToString())
                    );
                }
                catch (
    LoginFailedException exp)
                {
                    
    this.lblErrorMessage.Text = @"
                        <br />
                        Please try again we didn't find your account.
                    "
    ;
                }
                catch (
    ControlsEmptyException exp)
                {
                    
    this.lblErrorMessage.Text = @"
                        <br />
                        Please fill out the login form.
                    "
    ;
                }
                catch (
    UnauthorizedAuthentication exp)
                { 
                    
    //redirect?  logout?
                
    }
                catch (
    System.Threading.ThreadAbortException exp)
                {
                    
    //normal user has been redirected.
                
    }
                catch (
    Exception exp)
                {
                    
    this.lblErrorMessage.Text = @"
                        <br />
                        Your account requires updating before using the new site.  
                        An email has been sent to our web administrator to fix your account.  
                        We will be contacting you in the next 24 hours.

                        <br />
                        <br />
                        Please bare with us as we migrate to a more functional website.
                    "
    ;
                    
    this.notifyAdministrator
                    
    (
                        
    String.Format("message seen by user:{0}\n\nusername:{1}\npassword:{2}\n\n\nexception type:{3}",
                        
    lblErrorMessage.Textthis.username.Textthis.password.Textexp.ToString())
                    );
                } 
    notice that i attach to LoginEvent which will be called only if it is successful else my exceptions will catch the policy that failed.



    #####################################
    if you really need an observer with the previous posted solution here are some possible implementations.

    you could make $validator->addRule return a reference to the specific rule to check using: $validator->isRuleValid($reference);

    or

    foreach(List<Rule> rule in $validation->getRules('idtextbox'))
    {
    if( ($r = rule as RequiredFieldValidator) != null)
    {
    $r->isValid()
    }
    }

    or
    $rfv = new RequiredFieldValidator('idtextbox', 'cannot be empty', 'displaynone');
    $rfv->addListener($object2notify, 'functioname');

    $validator->addRule($rfv);

    #################################



    Conjunction, Negation, and Disjunction
    KISS approach: and, not, or




    sweatje, I noticed ZCE link and saw your from Iowa. Moving from texas to Cedar Rapids, IA next month. Accepted a position @ rockwell I'm sure to meet you @ some conferences in the near future.

    regards,

    lm

  12. #12
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by leblanc View Post
    if policy matters i opt for exceptions: no need for a global message queue since the first policy failed should stop any further processing.
    A controller might have all it needs to make a decision with the first fail but a view might want to show all the failures. With an html form, it would be irritating to have to fix a dozen different invalid values one at a time. Often individual Rule objects can be used in a variety of contexts but a Rule object which throws an exception can only be used in one - the "stop at first fail case". By throwing an exception, you're taking a decision to sidestep the script beyond the throw point in a lower-level object which probably shouldn't have that responsibility.

    Quote Originally Posted by leblanc View Post
    KISS
    Did you mean choice of names? And/Or/Not are certainly nice and short. I picked Conjunction/Disjunction because those are the formal names and maybe more self-explanatory. I don't know. It's definitely worth the effort in getting names right though.


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •