Jump to content

Recursively remove all array elements which are not of given type


Recommended Posts

Can anyone let me know what I am doing wrong.  I am sure it will (after the fact) be obvious, but I don't see it right now.  Wish to remove all array elements which do not implement ValidatorCallbackInterface.  Thanks

<?php
interface ValidatorCallbackInterface{}
class ValidatorCallback implements ValidatorCallbackInterface{}

function array_filter_recursive($input) {
    foreach ($input as &$value) {
        if (is_array($value)) {
            $value = array_filter_recursive($value);
        }
    }
    return array_filter($input, function($v) {
        return $v instanceOf ValidatorCallbackInterface;
    });
}

function recursive_unset(&$array) {
    foreach ($array as $key => $value) {
        if (is_array($value)) {
            recursive_unset($value);
            if(empty($value)) {
                unset($array[$key]);
            }
        }
        elseif(!$value instanceOf ValidatorCallbackInterface) {
            unset($array[$key]);
        }
    }
}

$validatorCallback = new ValidatorCallback();
$rules=[
    'callbackId'=>"integer",
    'info'=>[
        'arrayofobjects'=>[$validatorCallback],
        'foo1'=>'bar1'
    ],
    'foo2'=>'bar2',
    'bla'=>[
        'a'=>'aa',
        'b'=>'bb',
    ],
    'singleobject'=>$validatorCallback
];

echo('original rules'.PHP_EOL);
var_dump($rules);

$desiredrules=[
    'info'=>[
        'arrayofobjects'=>[$validatorCallback]
    ],
    'singleobject'=>$validatorCallback
];

echo('desired rules'.PHP_EOL);
var_dump($desiredrules);

echo('array_filter_recursive'.PHP_EOL);
var_dump(array_filter_recursive($rules));

echo('recursive_unset'.PHP_EOL);
recursive_unset($rules);
var_dump($rules);

 

original rules
array(5) {
  ["callbackId"]=>
  string(7) "integer"
  ["info"]=>
  array(2) {
    ["arrayofobjects"]=>
    array(1) {
      [0]=>
      object(ValidatorCallback)#1 (0) {
      }
    }
    ["foo1"]=>
    string(4) "bar1"
  }
  ["foo2"]=>
  string(4) "bar2"
  ["bla"]=>
  array(2) {
    ["a"]=>
    string(2) "aa"
    ["b"]=>
    string(2) "bb"
  }
  ["singleobject"]=>
  object(ValidatorCallback)#1 (0) {
  }
}
desired rules
array(2) {
  ["info"]=>
  array(1) {
    ["arrayofobjects"]=>
    array(1) {
      [0]=>
      object(ValidatorCallback)#1 (0) {
      }
    }
  }
  ["singleobject"]=>
  object(ValidatorCallback)#1 (0) {
  }
}
array_filter_recursive
array(1) {
  ["singleobject"]=>
  object(ValidatorCallback)#1 (0) {
  }
}
recursive_unset
array(2) {
  ["info"]=>
  array(2) {
    ["arrayofobjects"]=>
    array(1) {
      [0]=>
      object(ValidatorCallback)#1 (0) {
      }
    }
    ["foo1"]=>
    string(4) "bar1"
  }
  ["singleobject"]=>
  object(ValidatorCallback)#1 (0) {
  }
}

 

Link to comment
Share on other sites

function array_filter_recursive($input) {
    foreach ($input as &$value) {
        if (is_array($value)) {
            $value = array_filter_recursive($value);
        }
    }
    return array_filter($input, function($v) {
        return $v instanceOf ValidatorCallbackInterface;
    });
}

Arrays are not instances of ValidatorCallbackInterface.

Link to comment
Share on other sites

Agree, but I thought the custom array_filter_recursive() function would make it so.  This was only an interim step, and I am now thinking probably not a good way to go.

May actual end goal is convert an array to a stdClass and remove all whitespace from all the values.  Actually, my real end goal is to validate JSON given a set of rules defined in an array.

I was previously doing so using the following:

$rules=json_decode(str_replace(' ', '', json_encode($rules)));

But now, I wish to allow more than just stdClass objects.  Any other recommendations how to accomplish?  Thanks

Link to comment
Share on other sites

Maybe...

 

function array2Obj(array $rules, ?\stdClass $obj=null):\stdClass {
    $obj=$obj??new \stdClass;
    foreach($rules as $key => $rule) {
        if (is_array($rule)) {
            build($rule, $obj);
        }
        elseif(!$rule instanceOf ValidatorCallbackInterface) {
            $obj->$key=str_replace(' ', '', $rule);
        }
        else {
              $obj->$key=$rule;
        }
    }
    return $obj;
}

 

Link to comment
Share on other sites

1 minute ago, NotionCommotion said:

Agree, but I thought the custom array_filter_recursive() function would make it so.

How would that be? array_filter() isn't calling your recursive function. Yours applied itself recursively in the first part, which you needed to do, however in the second part all you're letting through is elements that obey the interface - which (sub)arrays will not do.

1 minute ago, NotionCommotion said:

May actual end goal is convert an array to a stdClass and remove all whitespace from all the values.

Then you need something to recursively edit the values. Like array_walk_recursive().

1 minute ago, NotionCommotion said:

  Actually, my real end goal is to validate JSON given a set of rules defined in an array.

Then it depends on how you need to validate. Maybe it'll be recursive, maybe it won't be.

Link to comment
Share on other sites

The validation already works.  My potential solution posted in my previous post however does not.  If an array is something like ['integer'], that means the JSON must have a sequential array of integers.  Doing the json_encode/decode trick takes care of this, but my previous attempt converts array ['integer'] to stdClass bla->0='integer'.

Think array_walk_recursive be the best fit?

Edited by NotionCommotion
Link to comment
Share on other sites

Why convert the array to an object? What's the point in changing one data structure type to another?

I don't think array_walk_recursive() is the answer to this current question. I imagine you need to walk a validation rules and the data in tandem. What's some real example you have to support?

Link to comment
Share on other sites

In your array_filter_recursive function this section of code:

    foreach ($input as &$value) {
        if (is_array($value)) {
            $value = array_filter_recursive($value);
        }
    }

Goes through all the first level $input elements an if it's an array, calls the function again recursively to update it's values.

Then this section of the code:

    return array_filter($input, function($v) {
        return $v instanceOf ValidatorCallbackInterface;
    });

Goes through all the first level elements of $input again and checks if it's an instance of ValidatorCallbackInterface.  If not, it drops it.

Is $input['info'] an instance of ValidatorCallbackInterface?  Nope, so it'll get dropped.  The only thing in your initial input that would pass that check is $input['singleobject'].

If you're going to make a generic array_filter_recursive function you may as well pass in the callback as an argument.  Then you'd formulate the function as:

function array_filter_recursive($input, $filter) {
    $newArray = [];
    foreach ($input as $index=>$value) {
        if (is_array($value)) {
            $newArray[$index] = array_filter_recursive($value, $filter);
        } else if ($filter($value)){
            $newArray[$index] = $value;
        }
    }

    return $newArray;
}

That builds a new array which preserves only the elements where the filter returns true.  For array elements it applies the filter recursively.  For non array elements it checks the filter function, adding item to the new array only if it returns true.

 

Link to comment
Share on other sites

5 minutes ago, requinix said:

Why convert the array to an object? What's the point in changing one data structure type to another?

I don't think array_walk_recursive() is the answer to this current question. I imagine you need to walk a validation rules and the data in tandem. What's some real example you have to support?

Why convert?  Because one otherwise I need to make changes to the following and two it is a little easier to determine whether the array is associate or sequential. 

Real examples?  Similar to the following.  I wrote the class to validate a while back, and am no longer very proud of it and probably need to rewrite it one day.

$rules=[
    'reconnectTimeout'=>"integer:betweenValue,5,300",
    'responseTimeout'=>"integer:betweenValue,5,300",
    'historyPackSize'=>"integer",
    'verboseLog'=>"boolean",
];

$arrayOfRules=[[
    'deviceId'=>"integer:betweenValue,0,4194302",
    'pollrate'=>"integer:betweenValue,5,600||exactValue,0",
    'lifetime'=>"integer:betweenValue,0,300"
]];


 

<?php
namespace NotionCommotion\JsonValidator;

class JsonValidatorException extends \Exception
{
    // Supported codes Configuration error: 0, Validation error: 1
    private $errors, $blueprint, $input;

    public function __construct($message, $code=0, Exception $previous = null, $errors=false, $blueprint=false, $input=false) {
        $this->errors=$errors;
        $this->blueprint=$blueprint;
        $this->input=$input;
        parent::__construct($message, $code, $previous);
    }

    public function getError($sep=', ') {
        return is_array($this->errors)?implode($sep,$this->errors):$this->errors;
    }

    public function getErrorArray() {
        return (array) $this->errors;
    }

    public function getBlueprint() {
        return $this->blueprint;
    }

    public function getInput() {
        return $this->input;
    }
}

class JsonValidator
{
    private $delimitor = '~';   //Internal use with object() for the replacement of parenthese content, but can be changed if conflicts with user data (not necessary?)
    private $strictMode=true;   //Currently only enfources that boolean is true/false
    private $sanitize=false;     //Whether to sanitize (i.e. 'false' is changed to false)

    public function __construct($config=[])
    {
        if(!is_array($config)) throw JsonValidatorException('Constructor must be provided an array or no value');
        if(array_diff($config,array_flip(['strictMode','delimitor','sanitize']))) throw JsonValidatorException('Invalid constructor value');
        foreach($config as $key=>$value) {
            $this->$key=$value;
        }
    }

    public function object($input, $blueprint){
        /*
        Description:
        Validateds JSON object's properties type and values based on a stdClass "blueprint" which specifies rules.
        The following describes the single public method "object()"

        Parameters:
        input. The JSON object (not associated array) to validate.
        blueprint.  The ruleset which the JSON object must follow.

        Return value:
        $input potentially sanitized

        Return Errors:
        Will be returned via a JsonValidatorException

        Ruleset:  Specifies the type and value of each property in the JSON object, and is also a JSON object.

        Types:
        Supported types: string, integer, double and boolean, object (stdClass only), and arrays.
        Note that double is used because of float since for historical reasons "double" is returned in case of a float, and not simply "float" (http://php.net/manual/en/function.gettype.php)
        Multiple types for a single property are not supported (except for general objects and arrays).
        Objects are specifying using an associated array using name/blueprint for each element.
        Sequential arrays are specified by a single element sequencial array which is used for all elements.
        Arrays and objects can be recursive.
        An asterisk "*" is for any type.
        If a string type starts with an tilde "~", it is optional.
        An object with any content is specified as an empty array [].
        An sequntial array with any content is specified by ['*']
        ## REMOVE: Special types "array", "object", "object|array", and "array|object" can be specified for the general type, and if so, will not be validated recursivly and value validation methods are limited to empty and notEmpty.

        Values:
        Value rules are specified by a validation method preceeded by a colon ":".
        can be validated by changing the desired type from type to type:validation_method
        Additional variables to include with the given validation_method can be specified by including a comma "," between them.
        Multiple validation methods can be used and must be separated by && for AND or || for OR.
        Order is based on 1) parenthese, 2) AND, and 3) OR.
        ! is used to negate.

        Example:
        ['method'=>'string:max,5||someOtherMethod,123','params'=>['paramString'=>'(~string:ip&&minLengh,5)'],'extra'=>['name'=>'string','test'=>['string']]]
        */

        if( !is_array($blueprint) && !(is_object($blueprint) && is_a($blueprint,'stdClass'))) throw new JsonValidatorException('Invalid blueprint provided.  Must be an array or stdClass object.');
        if( !is_array($input) && !(is_object($input) && is_a($input,'stdClass'))) throw new JsonValidatorException('Invalid input provided.  Must be an array or stdClass object.');
        $input=json_decode(json_encode($input));
        $blueprint=json_decode(str_replace(' ', '', json_encode($blueprint)));  //Rules cannot include accidental developer provided whitespace
        if(!$input && !$blueprint) return $input;
        $errors=$this->_object($input, $blueprint, 'base', []);
        if($errors) {
            throw new JsonValidatorException('Validation error', 1, null, $errors, $blueprint, $input);
        }
        return $input;
    }

    //Methods are protected and not private so that this class can be extended
    protected function _object($input, $blueprint, $level, array $errors){
        //$blueprint should only be an object or an unassociated array
        /*
        $xdb_input=json_encode($input);
        $xdb_blueprint=json_encode($blueprint);
        echo("<h1>$level</h1>");
        echo("<h4>input</h4>");
        echo('<pre>'.print_r($input,1).'</pre>');
        echo("<h4>blueprint</h4>");
        echo('<pre>'.print_r($blueprint,1).'</pre>');
        */
        if(is_array($blueprint)) {
            switch(count($blueprint)) {
                case 0:
                    //Can be an empty array or any object
                    if(is_array($input)) {
                        if(!empty($input)) {
                            $errors[]="Unexpected sequential array provided in the '$level' object.";
                        }
                    }
                    elseif(!is_object($input)) {
                        $errors[]="Unexpected value '$input' provided in the '$level' object.";
                    }
                    break;
                case 1:
                    if(is_array($input)) {

                        if(is_array($blueprint[0]) || is_object($blueprint[0])) {
                            $err=[];
                            foreach($input as $key=>$item) {
                                if($e=self::_object($item, $blueprint[0], $level.'['.$key.']', $errors)) {
                                    $e[]=$err;
                                }
                            }
                            if($err) {
                                $errors[]=implode(', ',$err);
                            }
                        }
                        else {

                            //String or value (coming from sequential array)
                            $rule=explode(':',$blueprint[0]); //[0=>typeRule,1=>validationRule]

                            if($rule[0]!='*') { // * means any type, so skip (value validation not avaiable)
                                foreach($input as $key=>$item) {
                                    if($this->sanitize) {
                                        $item=$this->sanitize($item, $rule[0]);
                                    }
                                    $type=gettype($item);
                                    if( $type!=$rule[0] && $this->strictBoolean($item,$rule[0])) {
                                        $errors[]="Sequential array value in the '$level' object is a $type but should be a $rule[0].";
                                    }
                                    elseif(count($rule)==2) {
                                        $rs=self::validateValue($item, $rule[1], $level.'['.$key.']', $this->delimitor);
                                        if(!$rs[0]) {
                                            $errors[]="Invalid value in the '$level' sequential array: ".$rs[1][0];
                                        }
                                    }
                                }
                            }
                        }
                    }
                    elseif(is_object($input)) {
                        $prop=implode(', ',array_keys((array)$input));
                        $errors[]="Unexpected property(s) '$prop' provided in the '$level' object.";
                    }
                    else {
                        $errors[]="Unexpected value '$input' provided in the '$level' object.";
                    }
                    break;
                default: throw new JsonValidatorException('Sequential array blueprint may only have one element');
            }
        }
        elseif(is_object($blueprint)) {
            if(!is_object($input)) {
                $prop=implode(', ',array_keys((array)$blueprint));
                $errors[]="Missing property(s) '$prop' provided in the '$level' object.";
                if(!is_array($input)) {
                    $errors[]="Unexpected sequential array provided in the '$level' object.";
                }
                else {
                    $errors[]="Unexpected value '$input' provided in the '$level' object.";
                }
            }
            else {
                if($extraKeys=array_diff(array_keys((array) $input), array_keys((array) $blueprint))) {
                    $prop=implode(', ',$extraKeys);
                    $errors[]="Unexpected property(s) '$prop' provided in the '$level' object.";
                }
                foreach($blueprint as $prop=>$rule){
                    if(is_object($rule)) {
                        if(!isset($input->$prop)) {
                            $errors[]="Missing object '$prop' in the '$level' object.";
                        }
                        elseif(!is_object($input->$prop)) {
                            $missingType=is_array($rule)?'sequential array':'value';
                            $errors[]="Unexpected $missingType property '$prop' in the '$level' object.";
                        }
                        else {
                            $errors=array_merge($errors,self::_object($input->$prop, $blueprint->$prop, $level.'['.$prop.']', $errors));
                        }
                    }
                    elseif(is_array($rule)) {
                        if(!isset($input->$prop)) {
                            $errors[]="Missing sequential array '$prop' in the '$level' object.";
                        }
                        elseif(!is_array($input->$prop)) {
                            $missingType=is_object($rule)?'object':'value';
                            $errors[]="Unexpected $missingType property '$prop' in the '$level' object.";
                        }
                        else {
                            $errors=array_merge($errors,self::_object($input->$prop, $rule, $level.'['.$prop.']', $errors));
                        }
                    }
                    elseif(isset($input->$prop) || $rule[0]!='~') { //Skip if optional and not provided in input

                        if($rule[0]=='~') {
                            $rule=substr($rule, 1);
                        }
                        $rule=explode(':',$rule); //[0=>typeRule,1=>validationRule]

                        if(!isset($input->$prop)) {
                            $errors[]="Missing property '$prop' in the '$level' object.";
                        }
                        elseif($rule[0]!='*') { // * means any type, so skip (value validation not avaiable)
                            if($this->sanitize) {
                                $input->$prop=$this->sanitize($input->$prop, $rule[0]);
                            }
                            $type=gettype($input->$prop);
                            if( $type!=$rule[0] && $this->strictBoolean($input->$prop,$rule[0])) {
                                $errors[]="Property '$prop' in the '$level' object is a $type but should be a $rule[0].";
                            }
                            elseif(count($rule)==2) {
                                $rs=self::validateValue($input->$prop, $rule[1], $prop, $this->delimitor);
                                if(!$rs[0]) {
                                    $errors[]="Invalid value for the '$prop' property in the '$level' object: ".$rs[1][0];
                                }
                            }
                        }
                        //else wildcard value is considered valid.
                    }
                    //else optional not provided value is considered valid.
                }
            }
        }
        else {
            throw new JsonValidatorException('Sanity check.  This should never occur.');
        }
        return $errors;
    }

    private function sanitize($value, $type) {
        switch($type) {
            case 'string':case 'object':case 'array':    //Not sanitized
                break;
            case 'integer':
                if(ctype_digit($value)) $value=(int)$value;
                break;
            case 'boolean':
                if(!is_bool($value)) $value=filter_var($value, FILTER_VALIDATE_BOOLEAN);
                break;
            case 'double':
                if(!is_float($value)) $value=filter_var($value, FILTER_VALIDATE_FLOAT);
                break;
            default: throw new JsonValidatorException("Invalid type '$type'");
        }
        return $value;
    }

    private function strictBoolean($value, $rule) {
        //returns false only if not in strictMode and testing boolean who's value is 0 or 1
        return $this->strictMode || $rule!='boolean' || !in_array($value,[0,1]);
    }

    static protected function validateValue($value, $ruleString, $prop, $delimitor)
    {
        //Store content in first tier parenthese and replace with delimitor
        if(strpos($ruleString, '(')) {
            preg_match_all('/\( ( (?: [^()]* | (?R) )* ) \)/x', $string, $match);   //$match[1] holds the results
            $ruleString=preg_replace('/\( ( (?: [^()]* | (?R) )* ) \)/x',$delimitor,$ruleString);
            $i=0;   //index for placeholders with parenthese
        }

        $errors=[];
        foreach(explode('||',$ruleString) as $orString) {
            foreach(explode('&&',$orString) as $rule) {

                if(substr($rule,0,1)=='!') {
                    $not=true;
                    $rule=substr($rule,1);
                }
                else $not=false;

                if(substr($rule,0,1)==$delimitor) {
                    syslog(LOG_ERR, 'JsonValidator::validateValue():  What does this do?');
                    $rs=self::validateValue($value,$match[1][$i++],$prop);
                }
                else {
                    $rule=explode(',',$rule);
                    $method=$rule[0];
                    unset($rule[0]);
                    if(!method_exists(get_called_class(), $method)) throw new JsonValidatorException("Method $method does not exist.");
                    $rs=self::$method($value,array_values($rule),$prop);
                }

                $rs[0]=$rs[0]!==$not;
                if(!$rs[0]) {
                    $errors[]=$rs[1];
                    break; //If in an AND group, one false makes all false so no need to continue
                }
            }
            if($rs[0]) break;     //If in an OR group, one true makes all true so no need to continue
        }
        if($rs[0])$rs[1]=[];
        else {
            //FUTURE.  Revise to return all errors with NOT state, and assemble when complete so that NOTs can be cancelled out
            $errors=implode(', ',$errors);
            $rs[1]=["Value of ".json_encode($value)." violates ".($not?"NOT($errors)":$errors)];
        }
        return $rs;
    }

    static protected function isAssocArray($arr)
    {
        //No longer used
        return $arr===[]?false:array_keys($arr) !== range(0, count($arr) - 1);
    }

    // ##################################################################
    //Each returns whether valid (true/false), error message, and negated error message
    static protected function empty($v, array $a, $prop)
    {
        if(count($a)!==0) throw new JsonValidatorException('Invalid arguement count.');
        return [empty((array)$v),"empty($prop)"];
    }
    static protected function minValue($v, array $a, $prop)
    {
        if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.');
        return [$v>=$a[0],"$prop>=$a[0]"];
    }
    static protected function maxValue($v, array $a, $prop)
    {
        if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.');
        return [$v<=$a[0],"$prop<=$a[0]"];
    }
    static protected function betweenValue($v, array $a, $prop)
    {
        if(count($a)!==2) throw new JsonValidatorException('Invalid arguement count.');
        sort($a);
        return [$v>=$a[0] && $v<=$a[1],"$a[0]>=$prop<=$a[1]"];
    }
    static protected function exactValue($v, array $a, $prop)
    {
        if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.');
        return [$v==$a[0],"$prop==$a[0]"];
    }
    static protected function minLength($v, array $a, $prop)
    {
        if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.');
        return [strlen(trim($v))>=$a[0],"strlen($prop)>=$a[0]"];
    }
    static protected function maxLength($v, array $a, $prop)
    {
        if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.');
        return [strlen(trim($v))<=$a[0],"strlen($prop)<=$a[0]"];
    }
    static protected function betweenLength($v, array $a, $prop)
    {
        if(count($a)!==2) throw new JsonValidatorException('Invalid arguement count.');
        $v=trim($v);
        sort($a);
        return [strlen($v)>=$a[0] && strlen($v)<$a[1],"$a[0]>=strlen($prop)<=$a[1]"];
    }
    static protected function exactLength($v, array $a, $prop)
    {
        if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.');
        return [strlen(trim($v))==$a[0],"strlen($prop)==$a[0]"];
    }

    static protected function email($v, array $a, $prop)
    {
        if(count($a)!==0) throw new JsonValidatorException('Invalid arguement count.');
        return [filter_var($v, FILTER_VALIDATE_EMAIL),"valid_email($prop)"];
    }
    static protected function url($v, array $a, $prop)
    {
        if(count($a)!==0) throw new JsonValidatorException('Invalid arguement count.');
        return [filter_var($v, FILTER_VALIDATE_URL),"valid_url($prop)"];
    }
    static protected function ipaddress($v, array $a, $prop)
    {
        if(count($a)!==0) throw new JsonValidatorException('Invalid arguement count.');
        return [filter_var($v, FILTER_VALIDATE_IP),"valid_ip($prop)"];
    }
}

 

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.