NotionCommotion Posted May 30, 2019 Share Posted May 30, 2019 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) { } } Quote Link to comment Share on other sites More sharing options...
requinix Posted May 30, 2019 Share Posted May 30, 2019 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. Quote Link to comment Share on other sites More sharing options...
NotionCommotion Posted May 30, 2019 Author Share Posted May 30, 2019 17 minutes ago, requinix said: Arrays are not instances of ValidatorCallbackInterface. Agree, but a sequential array element can be and I wanted the array with the single element to remain. Quote Link to comment Share on other sites More sharing options...
requinix Posted May 30, 2019 Share Posted May 30, 2019 Then my next hint is that array_filter() is not recursive. Quote Link to comment Share on other sites More sharing options...
NotionCommotion Posted May 30, 2019 Author Share Posted May 30, 2019 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 Quote Link to comment Share on other sites More sharing options...
NotionCommotion Posted May 30, 2019 Author Share Posted May 30, 2019 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; } Quote Link to comment Share on other sites More sharing options...
requinix Posted May 30, 2019 Share Posted May 30, 2019 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. Quote Link to comment Share on other sites More sharing options...
NotionCommotion Posted May 30, 2019 Author Share Posted May 30, 2019 (edited) 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 May 30, 2019 by NotionCommotion Quote Link to comment Share on other sites More sharing options...
requinix Posted May 30, 2019 Share Posted May 30, 2019 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? Quote Link to comment Share on other sites More sharing options...
kicken Posted May 30, 2019 Share Posted May 30, 2019 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. Quote Link to comment Share on other sites More sharing options...
NotionCommotion Posted May 30, 2019 Author Share Posted May 30, 2019 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)"]; } } Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.