ZF-4354: Custom array-handling strategies

Description

Zend_Filter_Input handles an associative array of input values. If one of these values is again an array then Zend_Filter_Input traverses the array and applies the filter or validator to every element. (It linearly traverses the first level and does not recursively traverse the array, in case it has multiple dimensions.) This behavior might make sense for common use cases, but it is only one way to handle arrays, which is hard-coded and forced. It does not allow fine-grained array handling and also does not allow to filter or validate array keys.

I propose a new boolean option called 'traverseArray' (or similar) that enables or disables the traversal. It defaults to true for backwards compatibility. Setting it to false makes Zend_Filter_Input pass values to validators or filters as they are, even if they are arrays. This allows array handling in the validator or filter. Common array-handling strategies, such as the default strategy described above, can be implemented using special meta validators and filters that decorate normal validators or filters as proposed by Bryce Lohr in http://nabble.com/Zend_Filter_Input-and-Arrays-td1… .

I already implemented the new feature based on ZF 1.6.1:


Index: C:/xampp/htdocs/snk/kalender.scoutnet.de/lib/ZendFramework/library/Zend/Filter/Input.php
===================================================================
--- C:/xampp/htdocs/snk/kalender.scoutnet.de/lib/ZendFramework/library/Zend/Filter/Input.php    (revision 11474)
+++ C:/xampp/htdocs/snk/kalender.scoutnet.de/lib/ZendFramework/library/Zend/Filter/Input.php    (working copy)
@@ -60,6 +60,7 @@
     const PRESENCE_REQUIRED = 'required';
     const RULE              = 'rule';
     const RULE_WILDCARD     = '*';
+    const TRAVERSE_ARRAY    = 'traverseArray';
     const VALIDATE          = 'validate';
     const VALIDATOR         = 'validator';
     const VALIDATOR_CHAIN   = 'validatorChain';
@@ -134,7 +135,8 @@
         self::ESCAPE_FILTER       => 'HtmlEntities',
         self::MISSING_MESSAGE     => "Field '%field%' is required by rule '%rule%', but the field is missing",
         self::NOT_EMPTY_MESSAGE   => "You must give a non-empty value for field '%field%'",
-        self::PRESENCE            => self::PRESENCE_OPTIONAL
+        self::PRESENCE            => self::PRESENCE_OPTIONAL,
+        self::TRAVERSE_ARRAY      => true
     );
 
     /**
@@ -516,7 +518,8 @@
                 case self::BREAK_CHAIN:
                 case self::MISSING_MESSAGE:
                 case self::NOT_EMPTY_MESSAGE:
-                case self::PRESENCE:
+                case self::PRESENCE:
+                case self::TRAVERSE_ARRAY:
                     $this->_defaults[$option] = $value;
                     break;
                 default:
@@ -565,7 +568,10 @@
             if (!isset($filterRule[self::FIELDS])) {
                 $filterRule[self::FIELDS] = $ruleName;
             }
-
+            if (!isset($filterRule[self::TRAVERSE_ARRAY])) {
+                $filterRule[self::TRAVERSE_ARRAY] = $this->_defaults[self::TRAVERSE_ARRAY];
+            }
+            
             /**
              * Load all the filter classes and add them to the chain.
              */
@@ -604,7 +610,7 @@
         if (!array_key_exists($field, $this->_data)) {
             return;
         }
-        if (is_array($this->_data[$field])) {
+        if ($filterRule[self::TRAVERSE_ARRAY] && is_array($this->_data[$field])) {
             foreach ($this->_data[$field] as $key => $value) {
                 $this->_data[$field][$key] = $filterRule[self::FILTER_CHAIN]->filter($value);
             }
@@ -708,6 +714,9 @@
             if (!isset($validatorRule[self::PRESENCE])) {
                 $validatorRule[self::PRESENCE] = $this->_defaults[self::PRESENCE];
             }
+            if (!isset($validatorRule[self::TRAVERSE_ARRAY])) {
+                $validatorRule[self::TRAVERSE_ARRAY] = $this->_defaults[self::TRAVERSE_ARRAY];
+            }
             if (!isset($validatorRule[self::ALLOW_EMPTY])) {
                 $validatorRule[self::ALLOW_EMPTY] = $this->_defaults[self::ALLOW_EMPTY];
             }
@@ -828,8 +837,8 @@
             }
         } else {
             $failed = false;
-            foreach ($data as $fieldKey => $field) {
-                if (!is_array($field)) {
+            foreach ($data as $fieldKey => $field) {
+               if (!$validatorRule[self::TRAVERSE_ARRAY] || !is_array($field)) {
                     $field = array($field);
                 }
                 foreach ($field as $value) {

class Zend_Filter_ArrayRecurse implements Zend_Filter_Interface{
    var $childFilter;
    public function __construct( $childFilter ){
        $this->childFilter = $childFilter;
    }
    public function filter( $value ){
        if( !is_array($value) ){
            return $this->childFilter->filter( $value );
        } else {
            foreach( $value as $key => $item ){
                $value[$key] = $this->filter( $item );
            }
            return $value;
        }
    }
}

class Zend_Filter_ArrayRecurseKeys implements Zend_Filter_Interface{
    var $childFilter;
    public function __construct( $childFilter ){
        $this->childFilter = $childFilter;
    }
    public function filter( $value ){
        if( !is_array($value) ){
            return $value;
        } else {
            $new_value = array();
            foreach( $value as $key => $item ){
                $new_value[ $this->childFilter->filter($key) ] = $this->filter( $item );
            }
            return $new_value;
        }
    }
}

/**
 * Filter for test purposes
 */
class FilterType implements Zend_Filter_Interface{
    public function filter( $value ){
        return gettype( $value );
    }
}

// input used in tests
$input = array( 'test' => array('a'=>array('a1')) );

// typical filter definition
$filters = array(
    'test' => array(new FilterType)
);

// Testing traverseArray enabled (default)
$i = new Zend_Filter_Input( $filters, array(), $input );
$expected_result = array(
    'test' => array( 'a' => 'array' )
);
assert( $expected_result == $i->getEscaped() );

// Testing traverseArray disabled
$i = new Zend_Filter_Input( $filters, array(), $input, array('traverseArray' => false) );
$expected_result = array(
    'test' => 'array'
);
assert( $expected_result == $i->getEscaped() );

// Testing recursive strategy filtering values
$filters = array(
    'test' => array( new Zend_Filter_ArrayRecurse(new FilterType) )
);
$i = new Zend_Filter_Input( $filters, array(), $input, array('traverseArray' => false) );
$expected_result = array(
    'test' => array( 'a' => array('string') )
);
assert( $expected_result == $i->getEscaped() );

// Testing recursive strategy filtering keys
$filters = array(
    'test' => array( new Zend_Filter_ArrayRecurseKeys(new FilterType) )
);
$i = new Zend_Filter_Input( $filters, array(), $input, array('traverseArray' => false) );
$expected_result = array(
    'test' => array( 'string' => array('integer' => 'a1') )
);
assert( $expected_result == $i->getEscaped() );

Comments

Any news with this? Traversing the values of an array makes working with arrays unfeasible, as example you cant serialize an input array without nesting it into a dummy array. The documentation does not state that values are traversed, is there a special reason for it?

The best solution would be to implement the option posted in the OP. or to remove the traversing logic completely.

Btw. http://framework.zend.com/issues/browse/ZF-6766 is a duplicate of this issue.

As you can see I took over all Zend_Filter_Input issues 6 days ago...

Sorry to say that but I can not fix 80 issues within 6 days... ;-) You will have to wait, as I am looking one after the other, into all of these issues.

Ah, thats great. I didnt look at the assignment date but encountered the problem a few hours ago and it took me a moment to find the culprit and thought i should report it, yada yada. Anyway i am watching this issue now :D

I think this is a pretty important issue. Nice to know there is going to be some progress. Thank you Thomas for taking over the assignment and looking into it.

Bulk change of all issues last updated before 1st January 2010 as "Won't Fix".

Feel free to re-open and provide a patch if you want to fix this issue.