D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
opt
/
psa
/
admin
/
plib
/
modules
/
git
/
vendor
/
netresearch
/
jsonmapper
/
src
/
Filename :
JsonMapper.php
back
Copy
<?php /** * Part of JsonMapper * * PHP version 5 * * @category Netresearch * @package JsonMapper * @author Christian Weiske <cweiske@cweiske.de> * @license OSL-3.0 http://opensource.org/licenses/osl-3.0 * @link http://cweiske.de/ */ /** * Automatically map JSON structures into objects. * * @category Netresearch * @package JsonMapper * @author Christian Weiske <cweiske@cweiske.de> * @license OSL-3.0 http://opensource.org/licenses/osl-3.0 * @link http://cweiske.de/ */ class JsonMapper { /** * 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 protected properties. * * @var boolean */ public $bIgnoreVisibility = false; /** * Remove attributes that were not passed in JSON, * to avoid confusion between them and NULL values. * * @var boolean */ public $bRemoveUndefinedAttributes = false; /** * Override class names that JsonMapper uses to create objects. * Useful when your setter methods accept abstract classes or interfaces. * * @var array */ public $classMap = array(); /** * 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 = array(); /** * Method to call on each object after deserialization is done. * * Is only called if it exists on the object. * * @var string|null */ public $postMappingMethod = null; /** * Optional arguments that are passed to the post mapping method * * @var array */ public $postMappingMethodArguments = array(); /** * Map data all data in $json into the given $object instance. * * @param object|array $json JSON object structure from json_decode() * @param object|class-string $object Object to map $json data into * * @return mixed Mapped object is returned. * @see mapArray() */ public function map($json, $object) { if ($this->bEnforceMapType && !is_object($json)) { throw new InvalidArgumentException( 'JsonMapper::map() requires first argument to be an object' . ', ' . gettype($json) . ' given.' ); } if (!is_object($object) && (!is_string($object) || !class_exists($object))) { throw new InvalidArgumentException( 'JsonMapper::map() requires second argument to ' . 'be an object or existing class name' . ', ' . gettype($object) . ' given.' ); } if (is_string($object)) { $object = $this->createInstance($object); } $strClassName = get_class($object); $rc = new ReflectionClass($object); $strNs = $rc->getNamespaceName(); $providedProperties = array(); 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, $isNullable) = $this->arInspectedClasses[$strClassName][$key]; if (!$hasProperty) { if ($this->bExceptionOnUndefinedProperty) { throw new JsonMapper_Exception( 'JSON property "' . $key . '" does not exist' . ' in object of type ' . $strClassName ); } else if ($this->undefinedPropertyHandler !== null) { $undefinedPropertyKey = call_user_func( $this->undefinedPropertyHandler, $object, $key, $jvalue ); if (is_string($undefinedPropertyKey)) { list($hasProperty, $accessor, $type, $isNullable) = $this->inspectProperty($rc, $undefinedPropertyKey); } } else { $this->log( 'info', 'Property {property} does not exist in {class}', array('property' => $key, 'class' => $strClassName) ); } if (!$hasProperty) { continue; } } if ($accessor === null) { if ($this->bExceptionOnUndefinedProperty) { throw new JsonMapper_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}', array('property' => $key, 'class' => $strClassName) ); continue; } if ($isNullable || !$this->bStrictNullTypes) { if ($jvalue === null) { $this->setProperty($object, $accessor, null); continue; } $type = $this->removeNullable($type); } else if ($jvalue === null) { throw new JsonMapper_Exception( 'JSON property "' . $key . '" in class "' . $strClassName . '" must not be NULL' ); } $type = $this->getFullNamespace($type, $strNs); $type = $this->getMappedType($type, $jvalue); 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) && !(is_array($jvalue) && $this->hasVariadicArrayType($accessor)) ) { if ($this->isFlatType($type) && !$this->isFlatType(gettype($jvalue)) ) { throw new JsonMapper_Exception( 'JSON property "' . $key . '" in class "' . $strClassName . '" is of type ' . gettype($jvalue) . ' and' . ' cannot be converted to ' . $type ); } settype($jvalue, $type); $this->setProperty($object, $accessor, $jvalue); continue; } //FIXME: check if type exists, give detailed error message if not if ($type === '') { throw new JsonMapper_Exception( 'Empty type at property "' . $strClassName . '::$' . $key . '"' ); } else if (strpos($type, '|')) { throw new JsonMapper_Exception( 'Cannot decide which of the union types shall be used: ' . $type ); } $array = null; $subtype = null; if ($this->isArrayOfType($type)) { //array $array = array(); $subtype = substr($type, 0, -2); } else if (substr($type, -1) == ']') { list($proptype, $subtype) = explode('[', substr($type, 0, -1)); if ($proptype == 'array') { $array = array(); } else { $array = $this->createInstance($proptype, false, $jvalue); } } else if (is_array($jvalue) && $this->hasVariadicArrayType($accessor)) { $array = array(); $subtype = $type; } else { if (is_a($type, 'ArrayAccess', true)) { $array = $this->createInstance($type, false, $jvalue); } } if ($array !== null) { if (!is_array($jvalue) && $this->isFlatType(gettype($jvalue))) { throw new JsonMapper_Exception( 'JSON property "' . $key . '" must be an array, ' . gettype($jvalue) . ' given' ); } $cleanSubtype = $this->removeNullable($subtype); $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 JsonMapper_Exception( 'JSON property "' . $key . '" must be an object, ' . gettype($jvalue) . ' given' ); } $child = $this->createInstance($type, true, $jvalue); } else { $child = $this->createInstance($type, false, $jvalue); $this->map($jvalue, $child); } $this->setProperty($object, $accessor, $child); } if ($this->bExceptionOnMissingData) { $this->checkMissingData($providedProperties, $rc); } if ($this->bRemoveUndefinedAttributes) { $this->removeUndefinedAttributes($object, $providedProperties); } if ($this->postMappingMethod !== null && $rc->hasMethod($this->postMappingMethod) ) { $refDeserializePostMethod = $rc->getMethod( $this->postMappingMethod ); $refDeserializePostMethod->setAccessible(true); $refDeserializePostMethod->invoke( $object, ...$this->postMappingMethodArguments ); } 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 === null || $type === '' || $type[0] === '\\' || $strNs === '') { return $type; } list($first) = explode('[', $type, 2); if ($this->isSimpleType($first)) { return $type; } //create a full qualified namespace return '\\' . $strNs . '\\' . $type; } /** * Check required properties exist in json * * @param array $providedProperties array with json properties * @param object $rc Reflection class to check * * @throws JsonMapper_Exception * * @return void */ protected function checkMissingData($providedProperties, ReflectionClass $rc) { foreach ($rc->getProperties() as $property) { $rprop = $rc->getProperty($property->name); $docblock = $rprop->getDocComment(); $annotations = static::parseAnnotations($docblock); if (isset($annotations['required']) && !isset($providedProperties[$property->name]) ) { throw new JsonMapper_Exception( 'Required property "' . $property->name . '" of class ' . $rc->getName() . ' is missing in JSON data' ); } } } /** * Remove attributes from object that were not passed in JSON data. * * This is to avoid confusion between those that were actually passed * as NULL, and those that weren't provided at all. * * @param object $object Object to remove properties from * @param array $providedProperties Array with JSON properties * * @return void */ protected function removeUndefinedAttributes($object, $providedProperties) { foreach (get_object_vars($object) as $propertyName => $dummy) { if (!isset($providedProperties[$propertyName])) { unset($object->{$propertyName}); } } } /** * 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 */ public function mapArray($json, $array, $class = null, $parent_key = '') { $originalClass = $class; foreach ($json as $key => $jvalue) { $class = $this->getMappedType($originalClass, $jvalue); if ($class === null) { $array[$key] = $jvalue; } else if ($this->isArrayOfType($class)) { $array[$key] = $this->mapArray( $jvalue, array(), 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 JsonMapper_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 ReflectionClass $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 * Fourth value: if the property is nullable */ 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) { $isNullable = false; $rparams = $rmeth->getParameters(); if (count($rparams) > 0) { $isNullable = $rparams[0]->allowsNull(); $ptype = $rparams[0]->getType(); if ($ptype !== null) { $typeName = $this->stringifyReflectionType($ptype); //allow overriding an "array" type hint // with a more specific class in the docblock if ($typeName !== 'array') { return array( true, $rmeth, $typeName, $isNullable, ); } } } $docblock = $rmeth->getDocComment(); $annotations = static::parseAnnotations($docblock); if (!isset($annotations['param'][0])) { return array(true, $rmeth, null, $isNullable); } list($type) = explode(' ', trim($annotations['param'][0])); return array(true, $rmeth, $type, $this->isNullable($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; $class = $rc; break; } } } if ($rprop !== null) { if ($rprop->isPublic() || $this->bIgnoreVisibility) { $docblock = $rprop->getDocComment(); if (PHP_VERSION_ID >= 80000 && $docblock === false && $class->hasMethod('__construct') ) { $docblock = $class->getMethod('__construct')->getDocComment(); } $annotations = static::parseAnnotations($docblock); if (!isset($annotations['var'][0])) { if (PHP_VERSION_ID >= 80000 && $rprop->hasType() && isset($annotations['param']) ) { foreach ($annotations['param'] as $param) { if (strpos($param, '$' . $rprop->getName()) !== false) { list($type) = explode(' ', $param); return array( true, $rprop, $type, $this->isNullable($type) ); } } } // If there is no annotations (higher priority) inspect // if there's a scalar type being defined if (PHP_VERSION_ID >= 70400 && $rprop->hasType()) { $rPropType = $rprop->getType(); $propTypeName = $this->stringifyReflectionType($rPropType); if ($this->isSimpleType($propTypeName)) { return array( true, $rprop, $propTypeName, $rPropType->allowsNull() ); } return array( true, $rprop, '\\' . ltrim($propTypeName, '\\'), $rPropType->allowsNull() ); } return array(true, $rprop, null, false); } //support "@var type description" list($type) = explode(' ', $annotations['var'][0]); return array(true, $rprop, $type, $this->isNullable($type)); } else { //no setter, private property return array(true, null, null, false); } } //no setter, no property return array(false, null, null, false); } /** * 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(array('_', '-'), ' ', $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 if (is_array($value) && $this->hasVariadicArrayType($accessor)) { $accessor->invoke($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 */ protected function createInstance( $class, $useParameter = false, $jvalue = null ) { if ($useParameter) { if (PHP_VERSION_ID >= 80100 && is_subclass_of($class, \BackedEnum::class) ) { return $class::from($jvalue); } return new $class($jvalue); } else { $reflectClass = new ReflectionClass($class); $constructor = $reflectClass->getConstructor(); if (null === $constructor || $constructor->getNumberOfRequiredParameters() > 0 ) { return $reflectClass->newInstanceWithoutConstructor(); } return $reflectClass->newInstance(); } } /** * Get the mapped class/type name for this class. * Returns the incoming classname if not mapped. * * @param string $type Type name to map * @param mixed $jvalue Constructor parameter (the json value) * * @return string The mapped type/class name */ protected function getMappedType($type, $jvalue = null) { if (isset($this->classMap[$type])) { $target = $this->classMap[$type]; } else if (is_string($type) && $type !== '' && $type[0] == '\\' && isset($this->classMap[substr($type, 1)]) ) { $target = $this->classMap[substr($type, 1)]; } else { $target = null; } if ($target) { if (is_callable($target)) { $type = $target($type, $jvalue); } else { $type = $target; } } return $type; } /** * 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' || $type === 'mixed'; } /** * 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, object and mixed) * * @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) === '[]'; } /** * Returns true if accessor is a method and has only one parameter * which is variadic. * * @param ReflectionMethod|ReflectionProperty|null $accessor accessor * to set value * * @return bool */ protected function hasVariadicArrayType($accessor) { if (!$accessor instanceof ReflectionMethod) { return false; } $parameters = $accessor->getParameters(); if (count($parameters) !== 1) { return false; } $parameter = $parameters[0]; return $parameter->isVariadic(); } /** * 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 ); } /** * Get a string representation of the reflection type. * Required because named, union and intersection types need to be handled. * * @param ReflectionType $type Native PHP type * * @return string "foo|bar" */ protected function stringifyReflectionType(ReflectionType $type) { if ($type instanceof ReflectionNamedType) { return ($type->isBuiltin() ? '' : '\\') . $type->getName(); } return implode( '|', array_map( function (ReflectionNamedType $type) { return ($type->isBuiltin() ? '' : '\\') . $type->getName(); }, $type->getTypes() ) ); } /** * Copied from PHPUnit 3.7.29, Util/Test.php * * @param string $docblock Full method docblock * * @return array Array of arrays. * Key is the "@"-name like "param", * each value is an array of the rest of the @-lines */ protected static function parseAnnotations($docblock) { $annotations = array(); // 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 = array()) { 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; } } ?>