File: /var/www/html/wptuneprotect/wp-content/plugins/insert-php/includes/jsonmapper/class-json-mapper.php
<?php
/**
* Part of WINP\JsonMapper\Mapper
*
* PHP version 5
*
* @author Christian Weiske <cweiske@cweiske.de>
* @package WINP\JsonMapper\Mapper
* @category Netresearch
* @license OSL-3.0 http://opensource.org/licenses/osl-3.0
* @link http://cweiske.de/
*/
namespace WINP\JsonMapper;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use ReflectionClass;
use ReflectionProperty;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Automatically map JSON structures into objects.
*
* @author Christian Weiske <cweiske@cweiske.de>
* @package WINP\JsonMapper\Mapper
* @category Netresearch
* @license OSL-3.0 http://opensource.org/licenses/osl-3.0
* @link http://cweiske.de/
*/
class Mapper {
/**
* PSR-3 compatible logger object
*
* @link http://www.php-fig.org/psr/psr-3/
* @var object
* @see setLogger()
*/
protected $logger;
/**
* Throw an exception when JSON data contain a property
* that is not defined in the PHP class
*
* @var boolean
*/
public $bExceptionOnUndefinedProperty = false;
/**
* Throw an exception if the JSON data miss a property
* that is marked with @required in the PHP class
*
* @var boolean
*/
public $bExceptionOnMissingData = false;
/**
* If the types of map() parameters shall be checked.
*
* You have to disable it if you're using the json_decode "assoc" parameter.
*
* json_decode($str, false)
*
* @var boolean
*/
public $bEnforceMapType = true;
/**
* Throw an exception when an object is expected but the JSON contains
* a non-object type.
*
* @var boolean
*/
public $bStrictObjectTypeChecking = false;
/**
* Throw an exception, if null value is found
* but the type of attribute does not allow nulls.
*
* @var bool
*/
public $bStrictNullTypes = true;
/**
* Allow mapping of private and proteted properties.
*
* @var boolean
*/
public $bIgnoreVisibility = false;
/**
* Override class names that WINP\JsonMapper\Mapper uses to create objects.
* Useful when your setter methods accept abstract classes or interfaces.
*
* @var array
*/
public $classMap = [];
/**
* Callback used when an undefined property is found.
*
* Works only when $bExceptionOnUndefinedProperty is disabled.
*
* Parameters to this function are:
* 1. Object that is being filled
* 2. Name of the unknown JSON property
* 3. JSON value of the property
*
* @var callable
*/
public $undefinedPropertyHandler = null;
/**
* Runtime cache for inspected classes. This is particularly effective if
* mapArray() is called with a large number of objects
*
* @var array property inspection result cache
*/
protected $arInspectedClasses = [];
/**
* Map data all data in $json into the given $object instance.
*
* @param object $json JSON object structure from json_decode()
* @param object $object Object to map $json data into
*
* @return object Mapped object is returned.
* @throws \ReflectionException
* @throws \WINP\JsonMapper\Exception
* @see mapArray()
*/
public function map( $json, $object ) {
if ( $this->bEnforceMapType && ! is_object( $json ) ) {
throw new InvalidArgumentException( 'WINP\JsonMapper\Mapper::map() requires first argument to be an object' . ', ' . gettype( $json ) . ' given.' );
}
if ( ! is_object( $object ) ) {
throw new InvalidArgumentException( 'WINP\JsonMapper\Mapper::map() requires second argument to be an object' . ', ' . gettype( $object ) . ' given.' );
}
$strClassName = get_class( $object );
$rc = new ReflectionClass( $object );
$strNs = $rc->getNamespaceName();
$providedProperties = [];
foreach ( $json as $key => $jvalue ) {
$key = $this->getSafeName( $key );
$providedProperties[ $key ] = true;
// Store the property inspection results so we don't have to do it
// again for subsequent objects of the same type
if ( ! isset( $this->arInspectedClasses[ $strClassName ][ $key ] ) ) {
$this->arInspectedClasses[ $strClassName ][ $key ] = $this->inspectProperty( $rc, $key );
}
list( $hasProperty, $accessor, $type ) = $this->arInspectedClasses[ $strClassName ][ $key ];
if ( ! $hasProperty ) {
if ( $this->bExceptionOnUndefinedProperty ) {
throw new Exception( 'JSON property "' . $key . '" does not exist' . ' in object of type ' . $strClassName );
} else if ( $this->undefinedPropertyHandler !== null ) {
call_user_func( $this->undefinedPropertyHandler, $object, $key, $jvalue );
} else {
$this->log( 'info', 'Property {property} does not exist in {class}', [
'property' => $key,
'class' => $strClassName
] );
}
continue;
}
if ( $accessor === null ) {
if ( $this->bExceptionOnUndefinedProperty ) {
throw new Exception( 'JSON property "' . $key . '" has no public setter method' . ' in object of type ' . $strClassName );
}
$this->log( 'info', 'Property {property} has no public setter method in {class}', [
'property' => $key,
'class' => $strClassName
] );
continue;
}
if ( $this->isNullable( $type ) || ! $this->bStrictNullTypes ) {
if ( $jvalue === null ) {
$this->setProperty( $object, $accessor, null );
continue;
}
$type = $this->removeNullable( $type );
} else if ( $jvalue === null ) {
//throw new Exception( 'JSON property "' . $key . '" in class "' . $strClassName . '" must not be NULL' );
}
if ( $type === null || $type === 'mixed' ) {
//no given type - simply set the json data
$this->setProperty( $object, $accessor, $jvalue );
continue;
} else if ( $this->isObjectOfSameType( $type, $jvalue ) ) {
$this->setProperty( $object, $accessor, $jvalue );
continue;
} else if ( $this->isSimpleType( $type ) ) {
if ( $type === 'string' && is_object( $jvalue ) ) {
throw new Exception( 'JSON property "' . $key . '" in class "' . $strClassName . '" is an object and' . ' cannot be converted to a string' );
}
settype( $jvalue, $type );
$this->setProperty( $object, $accessor, $jvalue );
continue;
}
//FIXME: check if type exists, give detailed error message if not
if ( $type === '' ) {
throw new Exception( 'Empty type at property "' . $strClassName . '::$' . $key . '"' );
}
$array = null;
$subtype = null;
if ( $this->isArrayOfType( $type ) ) {
//array
$array = [];
$subtype = substr( $type, 0, - 2 );
} else if ( substr( $type, - 1 ) == ']' ) {
list( $proptype, $subtype ) = explode( '[', substr( $type, 0, - 1 ) );
if ( ! $this->isSimpleType( $proptype ) ) {
$proptype = $this->getFullNamespace( $proptype, $strNs );
}
if ( $proptype == 'array' ) {
$array = [];
} else {
$array = $this->createInstance( $proptype, false, $jvalue );
}
} else {
$type = $this->getFullNamespace( $type, $strNs );
if ( is_a( $type, 'ArrayObject', true ) ) {
$array = $this->createInstance( $type, false, $jvalue );
}
}
if ( $array !== null ) {
if ( ! is_array( $jvalue ) && $this->isFlatType( gettype( $jvalue ) ) ) {
throw new Exception( 'JSON property "' . $key . '" must be an array, ' . gettype( $jvalue ) . ' given' );
}
$cleanSubtype = $this->removeNullable( $subtype );
if ( ! $this->isSimpleType( $cleanSubtype ) && $cleanSubtype !== null ) {
$subtype = $this->getFullNamespace( $cleanSubtype, $strNs );
}
$child = $this->mapArray( $jvalue, $array, $subtype, $key );
} else if ( $this->isFlatType( gettype( $jvalue ) ) ) {
//use constructor parameter if we have a class
// but only a flat type (i.e. string, int)
if ( $this->bStrictObjectTypeChecking ) {
throw new Exception( 'JSON property "' . $key . '" must be an object, ' . gettype( $jvalue ) . ' given' );
}
$type = $this->getFullNamespace( $type, $strNs );
$child = $this->createInstance( $type, true, $jvalue );
} else {
$type = $this->getFullNamespace( $type, $strNs );
$child = $this->createInstance( $type, false, $jvalue );
$this->map( $jvalue, $child );
}
$this->setProperty( $object, $accessor, $child );
}
if ( $this->bExceptionOnMissingData ) {
$this->checkMissingData( $providedProperties, $rc );
}
return $object;
}
/**
* Convert a type name to a fully namespaced type name.
*
* @param string $type Type name (simple type or class name)
* @param string $strNs Base namespace that gets prepended to the type name
*
* @return string Fully-qualified type name with namespace
*/
protected function getFullNamespace( $type, $strNs ) {
if ( $type !== '' && $type[0] != '\\' ) {
//create a full qualified namespace
if ( $strNs != '' ) {
$type = '\\' . $strNs . '\\' . $type;
}
}
return $type;
}
/**
* Check required properties exist in json
*
* @param array $providedProperties array with json properties
* @param object $rc Reflection class to check
*
* @return void
* @throws \WINP\JsonMapper\Exception
*
*/
protected function checkMissingData( $providedProperties, ReflectionClass $rc ) {
foreach ( $rc->getProperties() as $property ) {
$rprop = $rc->getProperty( $property->name );
$docblock = $rprop->getDocComment();
$annotations = $this->parseAnnotations( $docblock );
if ( isset( $annotations['required'] ) && ! isset( $providedProperties[ $property->name ] ) ) {
throw new Exception( 'Required property "' . $property->name . '" of class ' . $rc->getName() . ' is missing in JSON data' );
}
}
}
/**
* Map an array
*
* @param array $json JSON array structure from json_decode()
* @param mixed $array Array or ArrayObject that gets filled with
* data from $json
* @param string $class Class name for children objects.
* All children will get mapped onto this type.
* Supports class names and simple types
* like "string" and nullability "string|null".
* Pass "null" to not convert any values
* @param string $parent_key Defines the key this array belongs to
* in order to aid debugging.
*
* @return mixed Mapped $array is returned
* @throws \WINP\JsonMapper\Exception
*/
public function mapArray( $json, $array, $class = null, $parent_key = '' ) {
foreach ( $json as $key => $jvalue ) {
if ( $class === null ) {
$array[ $key ] = $jvalue;
} else if ( $this->isArrayOfType( $class ) ) {
$array[ $key ] = $this->mapArray( $jvalue, [], substr( $class, 0, - 2 ) );
} else if ( $this->isFlatType( gettype( $jvalue ) ) ) {
//use constructor parameter if we have a class
// but only a flat type (i.e. string, int)
if ( $jvalue === null ) {
$array[ $key ] = null;
} else {
if ( $this->isSimpleType( $class ) ) {
settype( $jvalue, $class );
$array[ $key ] = $jvalue;
} else {
$array[ $key ] = $this->createInstance( $class, true, $jvalue );
}
}
} else if ( $this->isFlatType( $class ) ) {
throw new Exception( 'JSON property "' . ( $parent_key ? $parent_key : '?' ) . '"' . ' is an array of type "' . $class . '"' . ' but contained a value of type' . ' "' . gettype( $jvalue ) . '"' );
} else if ( is_a( $class, 'ArrayObject', true ) ) {
$array[ $key ] = $this->mapArray( $jvalue, $this->createInstance( $class ) );
} else {
$array[ $key ] = $this->map( $jvalue, $this->createInstance( $class, false, $jvalue ) );
}
}
return $array;
}
/**
* Try to find out if a property exists in a given class.
* Checks property first, falls back to setter method.
*
* @param object $rc Reflection class to check
* @param string $name Property name
*
* @return array First value: if the property exists
* Second value: the accessor to use (
* ReflectionMethod or ReflectionProperty, or null)
* Third value: type of the property
*/
protected function inspectProperty( ReflectionClass $rc, $name ) {
//try setter method first
$setter = 'set' . $this->getCamelCaseName( $name );
if ( $rc->hasMethod( $setter ) ) {
$rmeth = $rc->getMethod( $setter );
if ( $rmeth->isPublic() || $this->bIgnoreVisibility ) {
$rparams = $rmeth->getParameters();
if ( count( $rparams ) > 0 ) {
$pclass = $rparams[0]->getClass();
$nullability = '';
if ( $rparams[0]->allowsNull() ) {
$nullability = '|null';
}
if ( $pclass !== null ) {
return [
true,
$rmeth,
'\\' . $pclass->getName() . $nullability
];
}
}
$docblock = $rmeth->getDocComment();
$annotations = $this->parseAnnotations( $docblock );
if ( ! isset( $annotations['param'][0] ) ) {
// If there is no annotations (higher priority) inspect
// if there's a scalar type being defined
if ( PHP_MAJOR_VERSION >= 7 ) {
$ptype = $rparams[0]->getType();
if ( $ptype !== null ) {
return [ true, $rmeth, $ptype . $nullability ];
}
}
return [ true, $rmeth, null ];
}
list( $type ) = explode( ' ', trim( $annotations['param'][0] ) );
return [ true, $rmeth, $type ];
}
}
//now try to set the property directly
//we have to look it up in the class hierarchy
$class = $rc;
$rprop = null;
do {
if ( $class->hasProperty( $name ) ) {
$rprop = $class->getProperty( $name );
}
} while( $rprop === null && $class = $class->getParentClass() );
if ( $rprop === null ) {
//case-insensitive property matching
foreach ( $rc->getProperties() as $p ) {
if ( ( strcasecmp( $p->name, $name ) === 0 ) ) {
$rprop = $p;
break;
}
}
}
if ( $rprop !== null ) {
if ( $rprop->isPublic() || $this->bIgnoreVisibility ) {
$docblock = $rprop->getDocComment();
$annotations = $this->parseAnnotations( $docblock );
if ( ! isset( $annotations['var'][0] ) ) {
return [ true, $rprop, null ];
}
//support "@var type description"
list( $type ) = explode( ' ', $annotations['var'][0] );
return [ true, $rprop, $type ];
} else {
//no setter, private property
return [ true, null, null ];
}
}
//no setter, no property
return [ false, null, null ];
}
/**
* Removes - and _ and makes the next letter uppercase
*
* @param string $name Property name
*
* @return string CamelCasedVariableName
*/
protected function getCamelCaseName( $name ) {
return str_replace( ' ', '', ucwords( str_replace( [ '_', '-' ], ' ', $name ) ) );
}
/**
* Since hyphens cannot be used in variables we have to uppercase them.
*
* Technically you may use them, but they are awkward to access.
*
* @param string $name Property name
*
* @return string Name without hyphen
*/
protected function getSafeName( $name ) {
if ( strpos( $name, '-' ) !== false ) {
$name = $this->getCamelCaseName( $name );
}
return $name;
}
/**
* Set a property on a given object to a given value.
*
* Checks if the setter or the property are public are made before
* calling this method.
*
* @param object $object Object to set property on
* @param object $accessor ReflectionMethod or ReflectionProperty
* @param mixed $value Value of property
*
* @return void
*/
protected function setProperty(
$object, $accessor, $value
) {
if ( ! $accessor->isPublic() && $this->bIgnoreVisibility ) {
$accessor->setAccessible( true );
}
if ( $accessor instanceof ReflectionProperty ) {
$accessor->setValue( $object, $value );
} else {
//setter method
$accessor->invoke( $object, $value );
}
}
/**
* Create a new object of the given type.
*
* This method exists to be overwritten in child classes,
* so you can do dependency injection or so.
*
* @param string $class Class name to instantiate
* @param boolean $useParameter Pass $parameter to the constructor or not
* @param mixed $jvalue Constructor parameter (the json value)
*
* @return object Freshly created object
*/
public function createInstance(
$class, $useParameter = false, $jvalue = null
) {
if ( isset( $this->classMap[ $class ] ) ) {
if ( is_callable( $mapper = $this->classMap[ $class ] ) ) {
$class = $mapper( $class, $jvalue );
} else {
$class = $this->classMap[ $class ];
}
}
if ( $useParameter ) {
return new $class( $jvalue );
} else {
return ( new ReflectionClass( $class ) )->newInstanceWithoutConstructor();
}
}
/**
* Checks if the given type is a "simple type"
*
* @param string $type type name from gettype()
*
* @return boolean True if it is a simple PHP type
*
* @see isFlatType()
*/
protected function isSimpleType( $type ) {
return $type == 'string' || $type == 'boolean' || $type == 'bool' || $type == 'integer' || $type == 'int' || $type == 'double' || $type == 'float' || $type == 'array' || $type == 'object';
}
/**
* Checks if the object is of this type or has this type as one of its parents
*
* @param string $type class name of type being required
* @param mixed $value Some PHP value to be tested
*
* @return boolean True if $object has type of $type
*/
protected function isObjectOfSameType( $type, $value ) {
if ( false === is_object( $value ) ) {
return false;
}
return is_a( $value, $type );
}
/**
* Checks if the given type is a type that is not nested
* (simple type except array and object)
*
* @param string $type type name from gettype()
*
* @return boolean True if it is a non-nested PHP type
*
* @see isSimpleType()
*/
protected function isFlatType( $type ) {
return $type == 'NULL' || $type == 'string' || $type == 'boolean' || $type == 'bool' || $type == 'integer' || $type == 'int' || $type == 'double' || $type == 'float';
}
/**
* Returns true if type is an array of elements
* (bracket notation)
*
* @param string $strType type to be matched
*
* @return bool
*/
protected function isArrayOfType( $strType ) {
return substr( $strType, - 2 ) === '[]';
}
/**
* Checks if the given type is nullable
*
* @param string $type type name from the phpdoc param
*
* @return boolean True if it is nullable
*/
protected function isNullable( $type ) {
return stripos( '|' . $type . '|', '|null|' ) !== false;
}
/**
* Remove the 'null' section of a type
*
* @param string $type type name from the phpdoc param
*
* @return string The new type value
*/
protected function removeNullable( $type ) {
if ( $type === null ) {
return null;
}
return substr( str_ireplace( '|null|', '|', '|' . $type . '|' ), 1, - 1 );
}
/**
* Copied from PHPUnit 3.7.29, Util/Test.php
*
* @param string $docblock Full method docblock
*
* @return array
*/
protected static function parseAnnotations( $docblock ) {
$annotations = [];
// Strip away the docblock header and footer
// to ease parsing of one line annotations
$docblock = substr( $docblock, 3, - 2 );
$re = '/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m';
if ( preg_match_all( $re, $docblock, $matches ) ) {
$numMatches = count( $matches[0] );
for ( $i = 0; $i < $numMatches; ++ $i ) {
$annotations[ $matches['name'][ $i ] ][] = $matches['value'][ $i ];
}
}
return $annotations;
}
/**
* Log a message to the $logger object
*
* @param string $level Logging level
* @param string $message Text to log
* @param array $context Additional information
*
* @return null
*/
protected function log( $level, $message, array $context = [] ) {
if ( $this->logger ) {
$this->logger->log( $level, $message, $context );
}
}
/**
* Sets a logger instance on the object
*
* @param LoggerInterface $logger PSR-3 compatible logger object
*
* @return null
*/
public function setLogger( $logger ) {
$this->logger = $logger;
}
}
?>