Observer pattern for form validation

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

Post Reply
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Observer pattern for form validation

Post by matthijs »

In a recent series of articles on DevShed Alejandro shows how to use the Observer pattern for form validation. A topic which comes up regular on this forum. For example, we have talked about using the Strategy pattern (see also here) for form validation. In the same thread, Arborint shows how he uses a Rules bases Validator class approach to Forms validation in his Skeleton. An approach which looks very solid. However, as I'm learning i'm also looking at other approaches.

I'll show the code from the devshed articles:
example:

Code: Select all

include 'DataValidator.class.php';
include 'FormObserver.class.php';

try{
    // instantiate 'FormObserver' object
    $formObs=new FormObserver('send');

    // instantiate validators and validate fields

    $alphaVal=new AlphaValidator($formObs);
    $alphaVal->validate('firstname','Enter a valid First Name (only alphabetic characters).');
    $alphaVal->validate('lastname','Enter a valid Last Name (only alphabetic characters).');

    $intVal=new IntegerValidator($formObs);
    $intVal->validate('age','Enter a valid age (0-99).');

    $emailVal=new EmailValidatorWin($formObs);
    $emailVal->validate('email','Enter a valid email address.');

    // check for errors
    $formObs->checkNotifications();
}
catch(Exception $e){
    echo $e->getMessage();
    exit();
}
FormObserver.class.php

Code: Select all

<?php 
/**
*  5-8-06
*  Centralizing the Validation of Data with the Observer Pattern in PHP
*  http://www.devshed.com/c/a/PHP/Centrali ... rn-in-PHP/ 
*  Alejandro
*/

// class FormObserver
class FormObserver{

    private $notifications;
    private $formVar;

    public function __construct($formVar){
        //$this->formVar=$_POST[$formVar];
          $this->formVar=!isset($_POST[$formVar])?'':$_POST[$formVar];
        $this->notifications=array();
    }

    public function addNotification($errorMessage){
        $this->notifications[]=$errorMessage;
    }

    public function checkNotifications(){
        if(!$this->formVar){
            $this->displayForm();
        }
        else
        {
            if(count($this->notifications)>0){
                // form is not OK
                $this->displayErrors();
                $this->displayForm();
            }
            else
            {
                // form is OK
                echo '<p>The form has been submitted successfully!</p>';
            }
        }
    }

    // display errors
    private function displayErrors(){
        $errstr='<p>Please correct the following fields and resubmit the form:</p>';
        foreach($this->notifications as $notification){
            $errstr.='<p>'.$notification.'</p>';
        }
        echo $errstr;
    }

    // display form
    public function displayForm(){
        $formstr='<form action="'.$_SERVER['PHP_SELF'].'" method="post">';
        $formstr.='First Name <input type="text" name="firstname" value="'.$_POST['firstname'].'"/><br />';
        $formstr.='Last Name <input type="text" name="lastname" value="'.$_POST['lastname'].'" /><br />';
        $formstr.='Email <input type="text" name="email" value="'.$_POST['email'].'" /><br />';
        $formstr.='Age <input type="text" size="1" name="age" value="'.$_POST['age'].'" /><br />';
        $formstr.='<input type="submit" value="Send" name="send" /></form>';

        echo $formstr;
    }
}
?>
DataValidator.class.php

Code: Select all

<?php 
/**
*  5-8-06
*  Centralizing the Validation of Data with the Observer Pattern in PHP
*  http://www.devshed.com/c/a/PHP/Centrali ... rn-in-PHP/ 
*  Alejandro
*/


// define DataValidator class
class DataValidator{

    protected $method;
    protected $formObserver;

    public function __construct(FormObserver $formObserver){
        $this->formObserver=$formObserver;
        $this->method=$_POST;
    }

    protected function notifyObserver($errorMessage){
        $this->formObserver->addNotification($errorMessage);
    }
}

// define StringValidator class
class StringValidator extends DataValidator{

    public function __construct($formObserver){
        parent::__construct($formObserver);
    }

    // validate strings
    public function validate($field,$errorMessage,$min=4,$max=32)
    {
        if(!isset($this->method[$field]) 
                 || trim($this->method[$field])=='' 
                 || strlen($this->method[$field])<$min
                 || strlen($this->method[$field])>$max)
        {
            $this->notifyObserver($errorMessage);
        }
    }
}

// define IntegerValidator class
class IntegerValidator extends DataValidator{

    public function __construct($formObserver){
        parent::__construct($formObserver);
    }
    // validate integers
    public function validate($field,$errorMessage){
        if(!isset($this->method[$field])
                || !is_numeric($this->method[$field])
                ||intval($this->method[$field])!=$this->method[$field]){
            $this->notifyObserver($errorMessage);
        }
    }
}

// define NumberValidator class
class NumberValidator extends DataValidator{

    public function __construct($formObserver){
        parent::__construct($formObserver);
    }

    // validate numbers
    public function validate($field,$errorMessage){
        if(!isset($this->method[$field])
               || !is_numeric($this->method[$field])){
            $this->notifyObserver($errorMessage);
        }
    }
}

// define RangeValidator class
class RangeValidator extends DataValidator{

    public function __construct($formObserver){
        parent::__construct($formObserver);
    }

    // validate ranges
    public function validate($field,$errorMessage,$min=1,$max=99){
        if(!isset($this->method[$field]) 
                || $this->method[$field]<$min || $this->method[$field]>$max){
            $this->notifyObserver($errorMessage);
        }
    }
}

// define AlphaValidator class
class AlphaValidator extends DataValidator{

    public function __construct($formObserver){
        parent::__construct($formObserver);
    }

    // validate alphabetic field
    public function validate($field,$errorMessage){
        if(!isset($this->method[$field]) 
                || !preg_match("/^[a-zA-Z]+$/",$this->method[$field])){
            $this->notifyObserver($errorMessage);
        }
    }
}

// define AlphanumValidator class
class AlphanumValidator extends DataValidator{

    public function __construct($formObserver){
        parent::__construct($formObserver);
    }
    
    // validate alphanumeric data
    public function validate($field,$errorMessage){
        if(!isset($this->method[$field]) 
                || !preg_match("/^[a-zA-Z0-9]+$/",$this->method[$field])){
            $this->notifyObserver($errorMessage);
        }
    }
}

// define EmailValidator class
class EmailValidator extends DataValidator{

    public function __construct($formObserver){
        parent::__construct($formObserver);
    }

    // validate email
    public function validate($field,$errorMessage){
        if(!isset($this->method[$field]) 
                || !preg_match("/.+@.+..+/",$this->method[$field])
                ||!checkdnsrr(array_pop(explode("@",$this->method[$field])),"MX")){
            $this->notifyObserver($errorMessage);
        }
    } 
}

// define EmailValidatorWin class (Windows systems)
class EmailValidatorWin extends DataValidator{

    public function __construct($formObserver){
        parent::__construct($formObserver);
    }

    public function validate($field,$errorMessage){
        if(!isset($this->method[$field]) 
               || !preg_match("/.+@.+..+/",$this->method[$field]) 
               || !$this->windnsrr(array_pop(explode("@",$this->method[$field])),"MX")){
            $this->notifyObserver($errorMessage);
        }
    }

    // private method 'windnsrr()' for Windows systems
    private function windnsrr($hostName,$recType=''){
        if(!empty($hostName)){
            if($recType=='')$recType="MX";
            exec("nslookup -type=$recType $hostName",$result);
            foreach($result as $line){
                if(preg_match("/^$hostName/",$line)){
                    return true;
                }
            }
            return false;
        }
        return false;
    }
}
?>
This approach seems to work ok. However, I can't see yet what advantages or disadvantages this pattern has in this situation compared to other solutions, like the previously mentioned Strategy Pattern or Rule Based approach from Arborint.

- I do like how the validation classes are nicely seperated. Each form of validation has it's own class. So that's ok.
- I don't like the fact that the form is hardcoded in the Observer class in the function displayForm. Suppose you have a whole bunch of forms, how would I refactor the code to seperate those?

As I'm not experienced enough to see other pitfalls I'm asking for your input. Just recently there was a thread about when to use the observer pattern, and form validation was not mentioned. Was that for a reason?

(p.s. if the mods want me to shorten the code I'll do that.)
User avatar
Ollie Saunders
DevNet Master
Posts: 3179
Joined: Tue May 24, 2005 6:01 pm
Location: UK

Post by Ollie Saunders »

shouldn't you have a $min and $max arg on you integer validator?
- I do like how the validation classes are nicely seperated. Each form of validation has it's own class. So that's ok.
- I don't like the fact that the form is hardcoded in the Observer class in the function displayForm. Suppose you have a whole bunch of forms, how would I refactor the code to seperate those?
I agree with you actually as I validation technique I don't like it much. If I use this as an example...

Code: Select all

$intVal=new IntegerValidator($formObs);
$intVal->validate('age','Enter a valid age (0-99).');
...I would prefer...

Code: Select all

$form->age->test(new IntegerValidator(0, 99), 'Please enter a valid age between 0 and 99');
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Post by matthijs »

shouldn't you have a $min and $max arg on you integer validator?
Yes. The min and max are in the validation class, but are missing from the example. I just copied the code from the tutorial.

Could you elaborate some more on why you don't like the technique/example/pattern?

I'm really trying to look at the big picture here. How using this pattern is useful and in which situation. Or, which problems I'll run into if I use this pattern in an application. The exact details of the code are not that interesting at this moment. I'm sure the examples have been thrown together as basic examples.
User avatar
Ollie Saunders
DevNet Master
Posts: 3179
Joined: Tue May 24, 2005 6:01 pm
Location: UK

Post by Ollie Saunders »

Well for a start I wouldn't say they example I gave is a specific not relevent to the bigger picture.
The difference between $formObject->method and $testType->validate($formObject) is profound and of great significance. Each represents a different focus or orientation, one where form entities are central or one where types of validation is central. With $testType->validate it is possible to achieve a much greater level of reusability and control over validations but almost everything else is negelated, HTML output and form structure for instance couldn't be easily built in; something you commented on yourself. With $formObject->method validation can be built in and form structure can be considered but perhaps you don't have as much structure over validation...actually no you can take a look at this:

Code: Select all

$f = new OF_Form('f');
{
    $txtFirstName = new OF_TextSmall('txtFirstName');
    // HTML and other aspects considered
    $txtFirstName->class = 'personal'; 
    $txtFirstName->events->change = $someJs;
    $txtFirstName->addFilter('alpha');
    $butSubmit = new OF_Button('butSubmit', 'Submit');
    
    $f->addEntities($txtFirstName, $butSubmit);
}
// ^ structure considered

if ($f->submitted()) {
    if ($txtFirstName->testIsEmpty()) { // built-in tests
        $txtFirstName->addError('Please specify your %label');
    } else {
        // custom tests
        class DbNameCheck extends OF_Validatation
        {
            public function test($value) 
            {
                // some test against db
                return true;
            }
        }
        $txtFirstName->test(new DbNameCheck(), true, 'Username taken');
    }
    if (!$f->getNumErrors()) {
        // db insert
    }
}
echo $f->render();
This is an example of what you are able to so with the form framework I am working on at the moment.
User avatar
sweatje
Forum Contributor
Posts: 277
Joined: Wed Jun 29, 2005 10:04 pm
Location: Iowa, USA

Post by sweatje »

Conseptually, the Observer pattern feels like a mismatch from the start. The observer lets you separate consumption of data from the producer of data. The pattern is also know as Publish-Subscribe. Is the change of state in the form an unexpected event you need to respond to? Do more than one different kind of observer need to respond to the changing data? It feels like perhaps application this is using this design pattern just for the sake of implementing it, not because the pattern solves problems you are experiencing in the code.
User avatar
Ollie Saunders
DevNet Master
Posts: 3179
Joined: Tue May 24, 2005 6:01 pm
Location: UK

Post by Ollie Saunders »

Conseptually, the Observer pattern feels like a mismatch from the start. The observer lets you separate consumption of data from the producer of data. The pattern is also know as Publish-Subscribe. Is the change of state in the form an unexpected event you need to respond to? Do more than one different kind of observer need to respond to the changing data? It feels like perhaps application this is using this design pattern just for the sake of implementing it, not because the pattern solves problems you are experiencing in the code.
OK, that's it, I'm gonna buy your book.
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Post by matthijs »

Thanks for your replies, Ole and Sweatje.
Is the change of state in the form an unexpected event you need to respond to?
No, it's an expected event. That's what the formpage is for.
Do more than one different kind of observer need to respond to the changing data?
No, I guess. It's one formpage with one response to it (with different outcomes of course).
It feels like perhaps application this is using this design pattern just for the sake of implementing it, not because the pattern solves problems you are experiencing in the code
That's a feeling I often get when reading articles online. Like all the foo-bar and bear-honey-tree examples.. :?

It's only when I start looking at a concrete example (like form validation in this case) that the patterns get a bit clearer. Now I'll crunch my brains some more looking at the code, Arborints code and your reply and book.

Ole: indeed, buy the book, it's good. I do have it, but for me it's still somewhat difficult so that's why I need some more help here :)
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Observer pattern for form validation

Post by Christopher »

The differences are interesting and I would be interested in hearing how each approach might produce benefits elsewhere in the code. The real benefit of this choice would be simplifying code elsewhere.

Here is observers style:

Code: Select all

$formObs=new FormObserver('send');
    $alphaVal=new AlphaValidator($formObs);
    $alphaVal->validate('firstname','Enter a valid First Name (only alphabetic characters).');
    if ($formObs->checkNotifications() ) {
Here is componenet style:

Code: Select all

$validator=new FormObserver('send');
    $alphaVal=new AlphaValidator('firstname', 'Enter a valid First Name (only alphabetic characters).');
    $validator->attach($alphaVal);
    if ($validator->check() ) {
One difference I can see is that the observer style checks the value immediately with validate(), whereas the component style does all the checks at the end with the check() method.
(#10850)
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Post by matthijs »

It's difficult to see the differences between the 2 examples.

The second code block you gave:

Code: Select all

//Here is componenet style:

    $validator=new FormObserver('send');
    $alphaVal=new AlphaValidator('firstname', 'Enter a valid First Name (only alphabetic characters).');
    $validator->attach($alphaVal);
    if ($validator->check() ) {
is essentially the same as your Rule based approach, isn't it?

Code: Select all

$validator = new Validator();
$validator->addRule(new RuleNotNull('username', 'Username required'));
$validator->addRule(new RuleNotNull('password', 'Password required'));
$validator->validate($_POST);
if ($validator->isValid()) { 
..
With a "component" style do you mean a strategy pattern?
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

matthijs wrote:It's difficult to see the differences between the 2 examples.
Because functionally there is none.
matthijs wrote:is essentially the same as your Rule based approach, isn't it?
Yes.
matthijs wrote:With a "component" style do you mean a strategy pattern?
There are multiple patterns going on here. I believe that the Strategy or Observer patterns would be about the behavior, whereas the Composite/Component would be about the structure. The decision about which to use would be dictated by the code around it.
(#10850)
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Post by matthijs »

Ok, thanks for the explanation. I'll read some more about those patterns now. Also discovered the http://www.patternsforphp.com site, some good material there.

It's interesting how patterns can be/are mixed and matched, how they aren't black and white solutions. If you look at an individual piece of code, multiple solutions are possible. But only after considering the environment is/might be one better then the other. Which are the best depend on everything going around them.
User avatar
Ollie Saunders
DevNet Master
Posts: 3179
Joined: Tue May 24, 2005 6:01 pm
Location: UK

Post by Ollie Saunders »

they aren't black and white solutions
Other than language syntax nothing in programming is, that's part of what makes it so interesting.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

matthijs wrote:If you look at an individual piece of code, multiple solutions are possible. But only after considering the environment is/might be one better then the other. Which are the best depend on everything going around them.
Context is everything in programming because it is the basis on which we make trade-offs. This is part of where the Refactoring and Test Driven movements come from. Refactoring is a way to change code while accepting the context and TDD makes the perspective from context to code so you write the test first (which creates the context) and then you code to that context.
(#10850)
Post Reply