Jump to content

Validate a stdClass object against a blueprint or prototype


NotionCommotion
 Share

Go to solution Solved by NotionCommotion,

Recommended Posts

I would like to validate a stdClass object against a blueprint or prototype of it.  This validation would be limited to ensuring that the object has identical attribute names (neither more or less) and attribute value types of the blueprint, but need not validate that the attribute values are correct.  The following seems to work, however, doesn't allow arrays in the object (which currently is acceptable, but who knows whether requirements will change).

 

Any better ways to do this?

 

Thanks

 

<?php
function validate($input,$blueprint,$level='base',$errors=[]){
    //Only validates types string, object, integer, double, and boolean but not array or resource
    $input=(array) $input;
    $countInput=count($input);
    $countBlueprint=count($blueprint);
    if($countInput!=$countBlueprint) {
        $errors[]="'$level' object has different count of properties";
    }
    else {
        foreach($input as $prop=>$value){
            $type=gettype($value);
            if($type=='object') {
                if(empty($blueprint[$prop]))$errors[]="Unexpected '$prop' provided in the '$level' object";
                elseif( ($typDesired=gettype($blueprint[$prop])) !='array') $errors[]="'$prop' in the '$level' object is an object but should be a $typDesired";
                else $errors=validate($value,$blueprint[$prop],"$level.$prop",$errors);
            }
            elseif( ($typDesired=$blueprint[$prop])!=$type)$errors[]="'$prop' in the '$level' object is a $type but should be a $typDesired";
        }
    }
    return $errors;
}


$given=(object)["method"=>"maintenance.Tshark",'params'=>(object)["paramString"=>" --guid d6f23460-0d77-400e-ae96-13f436e40245 --upload-server-ip 74.208.80.161 --upload-server-port 22 --upload-server-user bacnet --params -f 'port 1337 or port 47808' -i eth0 -a duration:5"],'extra'=>(object)["name"=>"filename"]];
$blueprint=['method'=>'string','params'=>['paramString'=>'string'],'extra'=>['name'=>'string']];
print_r(validate($given,$blueprint));

 

Link to comment
Share on other sites

What's wrong with array_intersect_key/assoc from the other thread?

I guess I thought I would have to iterate over the array to implement the recursive scope and identify objects.  Any ideas how to do otherwise?

 

PS.  I will be able to add validation for arrays.  Since the object is being created from JSON,, there will not be any associated arrays, and I can therefore check if the array associate or sequential array and act accordingly.

Link to comment
Share on other sites

  • Solution

I am sure it can be improved, but for what it is worth, this is what I will be doing.

<?php
function validate($input,$blueprint,$level='base',$errors=[]){
    /*Only validates types string, integer, double and boolean, object (stdClass only), and arrays but not resources.
    Arrays can not contain other arrays or objects, and all elements in the arrays must be of the same type
    Objects are validated by specifying an associated array for the object's attribute name and attribute type.
    */
    if(!is_object($input)) return ['validate method must be passed an object'];
    $input=(array) $input;
    if($missing=array_diff_key($blueprint,$input)) {
        $errors[]="'$level' object is missing properties '".implode(', ',array_flip($missing))."'.";
    }
    foreach($input as $prop=>$value){
        if(empty($blueprint[$prop])) $errors[]="Unexpected property '$prop' provided in the '$level' object.";
        else {
            $type=gettype($value);
            $typDesired=$blueprint[$prop];
            if(gettype($blueprint[$prop])=='array') {
                $isAssoc=$blueprint[$prop]===[]?false:array_keys($blueprint[$prop]) !== range(0, count($blueprint[$prop]) - 1);
                $typDesired=$isAssoc?'object':'array';
            }
            if( $type!=$typDesired) $errors[]="Property '$prop' in the '$level' object is a $type but should be a $typDesired.";
            elseif($type=='object') $errors=validate($value,$blueprint[$prop],"$level.$prop",$errors);
            elseif($type=='array') {
                foreach($value as $item) {
                    if( gettype($item)!=$blueprint[$prop][0]) {
                        $errors[]="At least one element of the '$prop' array in the '$level' object has the incorrect type and all must be a ".$blueprint[$prop][0].'.';
                        break;
                    }
                }
            }
        }
    }
    return $errors;
}


$json='{"method":"maintenance.Tshark","params":{"paramString":" --guid d6f23460-0d77-400e-ae96-13f436e40245 --upload-server-ip 74.208.80.161 --upload-server-port 22 --upload-server-user bacnet --params -f \'port 1337 or port 47808\' -i eth0 -a duration:5"},"extra":{"name":"filename","test":[1,4,7]}}';
$data=json_decode($json);
$blueprint=['method'=>'string','params'=>['paramString'=>'stringx'],'extra'=>['nasme'=>'string','test'=>['string']]];
echo('<pre>'.print_r(validate($data,$blueprint),1).'</pre>');
Array
(
    [0] => Property 'paramString' in the 'base.params' object is a string but should be a stringx.
    [1] => 'base.extra' object is missing properties 'nasme'.
    [2] => Unexpected property 'name' provided in the 'base.extra' object.
    [3] => At least one element of the 'test' array in the 'base.extra' object has the incorrect type and all must be a string.
)
Edited by NotionCommotion
Link to comment
Share on other sites

Thanks requnix,  Yeah, sometimes when I know something can be improved but will take a while, I like to take a breather before doing so.  Often when returning, I know more than I had before and say to myself "who wrote this hackish code, the 'right' way to do it is ...".

 

I ended up adding actual value validation as well.  For instance, the IP address is validated as a string as performed originally, but secondary optional validation can be added by appending a colon and the requirements such as string:maxLength,50|ipaddress  which will ensure that it is only 50 characters long an a IP.  I probably should give more thought to my specifying "language" for these rules, but oh well, will wait for another day...

$blueprint=[
    'mystring'=>'string:betweenLength,5,10',
    'myFirstObject'=>['anotherString'=>'string:maxLength,5','ip'=>'string:maxLength,50|ipaddress','someonesEmail'=>'string:email'],
    'mySecondObject'=>['myUrl'=>'string:url','anArray'=>['integer:maxValue,5']]
];
Hey, my answer was marked as the best answer.  I hope I don't come across as one of those people who ask a million questions, are spoon fed the answer, and then repost the provided answer to brag how smart they are :)
 
As always, appreciate the help!
Link to comment
Share on other sites

It's starting to get a bit unwieldy now. Moving to OOP would be nice. I would do it like:

 

- Validation class is given a specification/blueprint to follow

- Blueprint indicates all the information it needs to

- Value types are classes

- Value constraints could also be classes

 

Really quick and hacky example:

abstract class ValidatorOf {
	protected $spec;
	public function __construct($spec) { $this->spec = $spec; }
	public abstract function validate($key, $input);
}

class ValidatorOfString extends ValidatorOf {

	public function validate($key, $input) {
		if (!is_string($input)) {
			return ["{$key} is not a string"]; // abort
		}

		$e = [];
		if (isset($this->spec["minLength"]) && strlen($input) < $this->spec["minLength"]) {
			$e[] = "{$key} has a minimum length of {$this->spec['minLength']}, value has length of " . strlen($input);
		}
		if (isset($this->spec["maxLength"]) && strlen($input) > $this->spec["maxLength"]) {
			$e[] = "{$key} has a maximum length of {$this->spec['maxLength']}, value has length of " . strlen($input);
		}
		return $e;
	}

}

// class ValidatorOfIp extends ValidatorOf { ... }
// class ValidatorOfEmail extends ValidatorOf { ... }

class ValidatorOfObject extends ValidatorOf {

	private $properties = [];

	public function __construct($spec) {
		parent::__construct($spec);
		foreach ($spec["properties"] ?? [] as $prop => $pspec) {
			$this->properties[$prop] = Validator::createValidator($pspec);
		}
	}

	public function validate($key, $input) {
		if (!is_object($input)) {
			return ["{$key} is not an object"]; // abort
		} else if (isset($this->spec["class"]) && !is_a($input, $this->spec["class"])) {
			return ["{$key} must be an instance of {$this->spec['class']}, is " . get_class($input)]; // abort
		}

		$k = ($key ? $key . "." : "");
		$e = [];
		foreach ($this->properties as $prop => $validator) {
			if (!isset($input->$prop)) {
				$e[] = "{$k}{$prop} is missing";
			} else {
				$e = array_merge($e, $validator->validate($k . $prop, $input->$prop));
			}
		}
		return $e;
	}

}

class Validator {

	private $validator;

	public function __construct($spec) {
		$this->validator = self::createValidator($spec);
	}

	public function validate($input, &$errors = [], $name = "") {
		$errors = $this->validator->validate($name, $input);
		return !$errors;
	}

	public static function createValidator($spec) {
		$vclass = "ValidatorOf" . ucfirst($spec["type"]);
		return new $vclass($spec);
	}

}

$spec = [
	"type" => "object",
	"properties" => [
		"foo" => [
			"type" => "string",
			"maxLength" => 3
		]
	]
];

$obj = new stdClass();
$obj->foo = "four";

$v = new Validator($spec);
var_dump($v->validate($obj, $e, "obj"));
var_dump($e);
Link to comment
Share on other sites

This thread is more than a year old. Are you sure you have something important to add to it?

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.

 Share

×
×
  • 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.