Skip to content

Instantly share code, notes, and snippets.

@clicman
Created August 8, 2015 23:35
Show Gist options
  • Save clicman/960e685c450d6442024f to your computer and use it in GitHub Desktop.
Save clicman/960e685c450d6442024f to your computer and use it in GitHub Desktop.
<?php
namespace Stom\Api\Util\JsonMapper;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionMethod;
/**
* Part of JsonMapper
*
* PHP version 5
*
* @category Netresearch
* @package JsonMapper
* @author Christian Weiske <christian.weiske@netresearch.de>
* @license OSL-3.0 http://opensource.org/licenses/osl-3.0
* @link http://www.netresearch.de/
*/
/**
* Automatically map JSON structures into objects.
*
* @category Netresearch
* @package JsonMapper
* @author Christian Weiske <christian.weiske@netresearch.de>
* @license OSL-3.0 http://opensource.org/licenses/osl-3.0
* @link http://www.netresearch.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;
/**
* 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();
/**
* 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.
*/
public function map($json, $object) {
// if (!is_object($json)) {
// throw new InvalidArgumentException(
// 'JsonMapper::map() requires first argument to be an object'
// . ', ' . gettype($json) . ' given.'
// );
// }
if (!is_object($object)) {
throw new InvalidArgumentException(
'JsonMapper::map() requires second argument to be an object'
. ', ' . gettype($object) . ' given.'
);
}
$strClassName = get_class($object);
$rc = new ReflectionClass($object);
$strNs = $rc->getNamespaceName();
$providedProperties = array();
foreach ($json as $key => $jvalue) {
$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, $isSettable, $type, $setter) = $this->arInspectedClasses[$strClassName][$key];
if (!$hasProperty) {
if ($this->bExceptionOnUndefinedProperty) {
throw new JsonMapper_Exception(
'JSON property "' . $key . '" does not exist'
. ' in object of type ' . $strClassName
);
}
$this->log(
'info', 'Property {property} does not exist in {class}', array('property' => $key, 'class' => $strClassName)
);
continue;
}
if (!$isSettable) {
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 ($this->isNullable($type)) {
if ($jvalue === null) {
$this->setProperty($object, $key, null, $setter);
continue;
}
$type = $this->removeNullable($type);
}
if ($type === null || $type === 'mixed') {
//no given type - simply set the json data
$this->setProperty($object, $key, $jvalue, $setter);
continue;
} else if ($this->isObjectOfSameType($type, $jvalue)) {
$this->setProperty($object, $key, $jvalue, $setter);
continue;
} else if ($this->isSimpleType($type)) {
settype($jvalue, $type);
$this->setProperty($object, $key, $jvalue, $setter);
continue;
}
//FIXME: check if type exists, give detailled error message if not
if ($type === '') {
throw new JsonMapper_Exception(
'Empty type at property "'
. $strClassName . '::$' . $key . '"'
);
}
$array = null;
$subtype = null;
if (substr($type, -2) == '[]') {
//array
$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);
}
$array = new $proptype();
} else if ($type == 'ArrayObject' || is_subclass_of($type, 'ArrayObject')
) {
$array = new $type();
}
if ($array !== null) {
if (!$this->isSimpleType($subtype)) {
$subtype = $this->getFullNamespace($subtype, $strNs);
}
if ($jvalue === null) {
$child = null;
} else {
$child = $this->mapArray($jvalue, $array, $subtype);
}
} 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) {
$child = null;
} else {
$type = $this->getFullNamespace($type, $strNs);
$child = new $type($jvalue);
}
} else {
$type = $this->getFullNamespace($type, $strNs);
$child = new $type();
$this->map($jvalue, $child);
}
$this->setProperty($object, $key, $child, $setter);
}
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
*
* @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 = $this->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'
);
}
}
}
/**
* 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".
*
* @return mixed Mapped $array is returned
*/
public function mapArray($json, $array, $class = null) {
foreach ($json as $key => $jvalue) {
if ($class === null) {
$array[$key] = $jvalue;
} 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)) {
$array[$key] = $jvalue;
} else {
$array[$key] = new $class($jvalue);
}
}
} else {
$array[$key] = $this->map($jvalue, new $class());
}
}
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: is the property settable
* Third value: type of the property
* Fourth value: the setter to use, otherwise null
*/
protected function inspectProperty(ReflectionClass $rc, $name) {
//try setter method first
$setter = 'set' . str_replace(
' ', '', ucwords(str_replace('_', ' ', $name))
);
if ($rc->hasMethod($setter)) {
$rmeth = $rc->getMethod($setter);
if ($rmeth->isPublic()) {
$rparams = $rmeth->getParameters();
if (count($rparams) > 0) {
$pclass = $rparams[0]->getClass();
if ($pclass !== null) {
return array(
true, true, '\\' . $pclass->getName(), $rmeth
);
}
}
$docblock = $rmeth->getDocComment();
$annotations = $this->parseAnnotations($docblock);
if (!isset($annotations['param'][0])) {
return array(true, true, null, $rmeth);
}
list($type) = explode(' ', trim($annotations['param'][0]));
return array(true, true, $type, $rmeth);
}
}
//now try to set the property directly
if ($rc->hasProperty($name)) {
$rprop = $rc->getProperty($name);
if ($rprop->isPublic()) {
$docblock = $rprop->getDocComment();
$annotations = $this->parseAnnotations($docblock);
if (!isset($annotations['var'][0])) {
return array(true, true, null, null);
}
//support "@var type description"
list($type) = explode(' ', $annotations['var'][0]);
return array(true, true, $type, null);
} else {
//no setter, private property
return array(true, false, null, null);
}
}
//no setter, no property
return array(false, false, null, null);
}
/**
* 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 string $name Property name
* @param mixed $value Value of property
* @param object $setter The setter to use, null if no setter
* should be used
*
* @return void
*/
protected function setProperty(
$object, $name, $value, ReflectionMethod $setter = null
) {
if ($setter === null) {
$object->$name = $value;
} else {
$object->{$setter->getName()}($value);
}
}
/**
* 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
*/
protected function isSimpleType($type) {
return $type == 'string' || $type == 'boolean' || $type == 'bool' || $type == 'integer' || $type == 'int' || $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
*/
protected function isFlatType($type) {
return $type == 'NULL' || $type == 'string' || $type == 'boolean' || $type == 'bool' || $type == 'integer' || $type == 'int' || $type == 'double';
}
/**
* 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) {
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 = 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;
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment