电子邮件阵列上的PHP OOP过滤器验证

I'm using a validation class by David Powers (from the book PHP Object Oriented Solutions) for filtering input data. Below is a function to filter a single email address. How do I modify this function to filter an array of email addresses? (I have a dynamic form which may contain a variable number of email addresses and some other data relating to it: both arrays are the same length and these filters should not remove values from the arrays, just to report if any of the values are invalid.)

/**
 * Checks whether the input conforms to the format of an email address.
 * 
 * It does not check whether the address is genuine. Multiple addresses are
 * rejected, guarding against email header injection attacks.
 * 
 * @param string $fieldName Name of submitted value to be checked.
 */
public function isEmail($fieldName)
{
    // Check that another validation test has not been applied to the same input
    $this->checkDuplicateFilter($fieldName);
    // Set the filter options 
    $this->_filterArgs[$fieldName] = FILTER_VALIDATE_EMAIL;
}

Here's the function for validating integers. What would it require to make this function accept an array of integers? (I commented out the min, max options from the function. They are not mandatory in this array validator. But feel free to help in one way or another...)

public function isInt($fieldName/*, $min = null, $max = null*/)
{
    // Check that another validation test has not been applied to the same input
    $this->checkDuplicateFilter($fieldName);
    // Set the filter options
    $this->_filterArgs[$fieldName] = array('filter' => FILTER_VALIDATE_INT);
    // Add filter options to test for integers within a specified range
    //if (is_int($min)) {
    //    $this->_filterArgs[$fieldName]['options']['min_range'] = $min;
    //}
    //if (is_int($max)) {
    //    $this->_filterArgs[$fieldName]['options']['max_range'] = $max;
    //}
}

These were two of the public methods of validation class. Please let me know if I should attach the class constructor or the public function which actually performs the validation. Right now I'm guessing you don't need to see them but my OOP skills are quite poor. Any help is very much appreciated.

UPDATED: added the constructor

/**
 * Constructs a validator object for $_POST or $_GET input.
 * 
 * The constructor checks the availability of the PHP filter functions for which it
 * acts as a wrapper. If the optional first argument (an array of required fields) is 
 * supplied, it checks that each one contains at least some input. Any missing items are
 * stored as an array in the $missing property.
 * 
 * By default, the constructor sets the input type to "post". To create a validator for
 * the $_GET array, set the optional second argument to "get".
 * 
 * @param array  $required   Optional array containing the names of required fields or URL variables.
 * @param string $inputType  Type of input; valid options: "post" and "get" (case-insensitive); defaults to "post"
 * @return Pos_Validator object
 */
public function __construct($required = array(), $inputType = 'post')
{
    if (!function_exists('filter_list')) {
        throw new Exception('The Pos_Validator class requires the Filter Functions in >= PHP 5.2 or PECL.');
    }
    if (!is_null($required) && !is_array($required)) {
        throw new Exception('The names of required fields must be an array, even if only one field is required.');
    }
    $this->_required = $required;
    $this->setInputType($inputType);
    if ($this->_required) {
        $this->checkRequired();
    }
    $this->_filterArgs = array();
    $this->_errors = array();
    $this->_booleans = array();
}

UPDATE 2: I realized that after filtering any invalid array values are labeled as bool(false). So the function below works but it doesn't generate validation error same way as the single value filter functions do.

public function isIntArray($fieldName)
{
    // Check that another validation test has not been applied to the same input
    $this->checkDuplicateFilter($fieldName);
    // Set the filter options
    $this->_filterArgs[$fieldName]['filter'] = FILTER_VALIDATE_INT;
    $this->_filterArgs[$fieldName]['flags'] = FILTER_REQUIRE_ARRAY;

    //is is possible to mark the whole array to false or
    //somehow break the whole script?
}

The function above strips away invalid values and database cells are filled with blanks if no other precaution are taken. I think I'll check the validated array (with foreach loop) after filtering and if any of the array values is false, I break the script somehow. (There's no point inserting empty values in the database is they suppose to be valid email addresses or integers.) This solution works for now but I bet there is a more elegant and efficient way to do this. Thank you for your help and suggestions. I'm still open for new ideas.

-Jouni

UPDATED, see below.

I think I have a acceptable solution for this so I just answer this little more precisely than in the comments. Here's the whole class but I made some modifications to it. http://codepad.org/sCC0sruO

First I added a new protected value to the class: protected $_arrayHasFalse;

These two validating functions valide arrays of values. They mark the invalid values as false so a little more work has to be done.

public function isIntArray($fieldName) {
// Check that another validation test has not been applied to the same input
$this->checkDuplicateFilter($fieldName);
// Set the filter options
$this->_filterArgs[$fieldName]['filter'] = FILTER_VALIDATE_INT;
$this->_filterArgs[$fieldName]['flags'] = FILTER_REQUIRE_ARRAY;
}

public function isEmailArray($fieldName){
// Check that another validation test has not been applied to the same input
$this->checkDuplicateFilter($fieldName);
// Set the filter options
$this->_filterArgs[$fieldName]['filter'] = FILTER_VALIDATE_EMAIL;
$this->_filterArgs[$fieldName]['flags'] = FILTER_REQUIRE_ARRAY;
}

This function checks if any of the array values is invalid. It assigns the arrayHasFalse to true if everything is not valid. (This is a minor flaw as it does not remember all the invalid values. But I'm using it in a context where everything should validate or nothing is inserted into database so I am ok with this solution.)

public function checkArrayForFalseValues($array){
  foreach($array as $key => $value){
    if ($value == false){
      $this->_arrayHasFalse = true;
    }
  }
}

Finally we need a getter function.

public function getArrayFalseRevealer(){
  return $this->_arrayHasFalse;
}

So this is the deal in the form process file...

$val = new validator;
$val->isIntArray('AllShouldBeIntegers'); //dynamically expandable form field
$val->isEmailArray('AllShouldBeEmails'); //dynamically expandable form field

//these are in the original class (pretty self explanatory functions I think)
//validated input is stored in $filtered
$filtered = $val->validateInput();
$missing = $val->getMissing();
$errors = $val->getErrors();

//after the validation we go through a bit more work
$val->checkArrayForFalseValues($filtered['AllShouldBeIntegers']);
$val->checkArrayForFalseValues($filtered['AllShouldBeEmails']);

//mastervalue is set to true if anything is invalid
//maybe this function should be named: $doesAnyCheckedArrayHasEvenASingleInvalidValue
$arrayHasFalse = $val->getArrayFalseRevealer();

//proceed if everything is OK
if (!$missing && !$errors && !$arrayHasFalse) {// Everything passed validation.
  //do the database magic here...
}

I guess this does the trick. I'll mark this as an accepted answer. Feel free to comment if you have suggestions for improvements.

Thanks everyone.

UPDATE: Improved the array validator. Let's pass the filter as an argument (or is it a parameter?). It is called for example like this: $val->isArrayAcceplable('AllShouldBeEmails', 'email');

public function isArrayAcceplable($fieldName, $filter){
//Check that another validation test has not been applied to the same input
$this->checkDuplicateFilter($fieldName);

 //set the desired filter
 //add more options for your convenience
switch($filter){
  case 'email':
    $this->_filterArgs[$fieldName]['filter'] = FILTER_VALIDATE_EMAIL;
    break;
  case 'int':
    $this->_filterArgs[$fieldName]['filter'] = FILTER_VALIDATE_INT;
    break;
}

//we are validating an array
$this->_filterArgs[$fieldName]['flags'] = FILTER_REQUIRE_ARRAY;
}

//just remember to check the array for false values. (See above.)

-Jouni

You could just call the isInt function multiple times and have the loop on the outside of the function. What does the constructor look like?

If you really want to do it you way you could change the function to look something like this:

public function isInt( $my_array ) {
   foreach( $my_array as $element ) {
      $this->checkDuplicateFilter( $element );
      $this->_filterArgs[$element] = array('filter' => FILTER_VALIDATE_INT);
   }
}

If you like, post the constructor on here and we can show you how to instantiate it and validate your inputs that way instead.

UPDATE (Now constructor has been posted)

What's the name of the class? Let's assume it's called Validator (change to what it is really called)

$validator = new Validator( /* Add your optional arguments here */ );

/* change $inputs to the name of the array containing all your inputs */
foreach( $inputs as $input ) {
   // validate each input
   if( $validator->isInt( $input ) ) {
      // int is valid, do something
   } else {
      // int isn't valid, do something else
   }
}

Then do something similar for isEmail for your e-mail inputs.

you can also use array_map.

From php.net:

<?php
function cube($n)
{
    return($n * $n * $n);
}

$a = array(1, 2, 3, 4, 5);
$b = array_map("cube", $a);
print_r($b);
?>
Array
(
    [0] => 1
    [1] => 8
    [2] => 27
    [3] => 64
    [4] => 125
)

So in your case, you'd use: (Updated to use object)

array_map(array($this, $this->isEmail), $emailArray);
array_map(array($this, $this->isInt), $integerArray);

Fairly simple I think...